From d41ee504caedbe858e24ab2a23c7a804454c64c8 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 18 Jul 2026 02:15:29 +0200 Subject: Add Trash affordances: cockpit, listing, dashboard entry nodes#show gains a Trash section on trashed nodes: provenance from the trash entry, a restore form whose parent picker pre-fills the old parent while it still lives, and permanent deletion. A Move-to-Trash button joins the status actions on living nodes. nodes#trashed lists trashed subtree roots with weight, provenance, and deletion; the dashboard housekeeping row links to it, and trash/destroy redirect there. Deletion from Trash now removes the whole subtree, deepest first, each node through a real destroy! so every per-node cascade runs -- amending the never-recursive rule for this one sanctioned path (both confirms state the count; the root entry carries destroyed_descendants). Bare Node#destroy still refuses children. --- test/controllers/nodes_controller_test.rb | 31 +++++++++++++++++++---- test/models/node_trash_test.rb | 41 +++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 10 deletions(-) (limited to 'test') diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 7e5a990..6e2ddb3 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb @@ -427,11 +427,11 @@ class NodesControllerTest < ActionController::TestCase test "show never renders a destroy link for events" do login_as :quentin node = create_node_with_published_page - Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) + event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) get :show, params: { id: node.id } assert_response :success - assert_select "form.button_to.destructive", count: 0 + assert_select "form[action=?]", event_path(event), count: 0 end test "reverting from nodes#show returns to the show page, not the editor, even if a draft remains" do @@ -479,7 +479,8 @@ class NodesControllerTest < ActionController::TestCase login_as :quentin get :show, params: { :id => node.id } assert_response :success - assert_select "form.destructive", :count => 0 + assert_select "form[action=?]", revert_node_path(node), count: 0 + assert_select "form[action=?]", trash_node_path(node), count: 1 end test "drafts includes a never-published node with only a draft" do @@ -679,7 +680,7 @@ class NodesControllerTest < ActionController::TestCase put :trash, params: { :id => node.id } - assert_redirected_to node_path(Node.trash) + assert_redirected_to trashed_nodes_path assert node.reload.in_trash? end @@ -722,6 +723,26 @@ class NodesControllerTest < ActionController::TestCase delete :destroy, params: { :id => node.id } assert_not Node.exists?(node.id) - assert_redirected_to node_path(Node.trash) + assert_redirected_to trashed_nodes_path + end + + test "trash lists trashed subtree roots" do + login_as :quentin + node = Node.root.children.create!(:slug => "listed_in_trash") + node.trash!(users(:quentin)) + + get :trashed + assert_response :success + assert_select "a[href=?]", node_path(node) + end + + test "trashed rows carry provenance and a delete for childless roots" do + login_as :quentin + node = Node.root.children.create!(:slug => "provenance_test") + node.trash!(users(:quentin)) + + get :trashed + assert_select "td", /quentin/ + assert_select "form[action=?]", node_path(node), count: 1 end end diff --git a/test/models/node_trash_test.rb b/test/models/node_trash_test.rb index b72770b..07e5bc6 100644 --- a/test/models/node_trash_test.rb +++ b/test/models/node_trash_test.rb @@ -102,12 +102,25 @@ class NodeTrashTest < ActiveSupport::TestCase assert Node.exists?(node.id) end - test "destroy_from_trash! refuses nodes with children" do - node = create_node_with_published_page - node.children.create!(:slug => "still_here") - node.trash!(@user1) + test "destroy_from_trash! removes the whole subtree bottom-up with one entry" do + parent = create_node_with_published_page + child = parent.children.create!(:slug => "doomed_child") + grandchild = child.children.create!(:slug => "doomed_grandchild") + page_ids = [parent, child, grandchild].flat_map { |n| n.pages.pluck(:id) } + parent.trash!(@user1) + + assert_difference "NodeAction.count", 1 do + parent.reload.destroy_from_trash!(@user1) + end + + assert_not Node.exists?(parent.id) + assert_not Node.exists?(child.id) + assert_not Node.exists?(grandchild.id) + assert_equal 0, Page.where(:id => page_ids).count - assert_raises(ActiveRecord::RecordNotDestroyed) { node.reload.destroy_from_trash!(@user1) } + action = NodeAction.where(:action => "destroy").last + assert_equal 2, action.metadata["destroyed_descendants"] + assert_nil action.node_id end test "destroy_from_trash! logs an entry that survives the node" do @@ -124,4 +137,22 @@ class NodeTrashTest < ActiveSupport::TestCase assert_equal "Doomed", action.subject_name assert_equal @user1, action.user end + + test "suggested_restore_parent finds the old parent while it lives" do + parent = Node.root.children.create!(:slug => "old_home") + node = parent.children.create!(:slug => "comes_back") + node.trash!(@user1) + + assert_equal parent, node.reload.suggested_restore_parent + + parent.trash!(@user1) + assert_nil node.reload.suggested_restore_parent + end + + test "suggested_restore_parent is root for trashed top-level nodes" do + node = Node.root.children.create!(:slug => "was_top_level") + node.trash!(@user1) + + assert_equal Node.root, node.reload.suggested_restore_parent + end end -- cgit v1.3