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. --- app/views/admin/index.html.erb | 4 +++ app/views/nodes/show.html.erb | 53 +++++++++++++++++++++++++++++++++++++++- app/views/nodes/trashed.html.erb | 42 +++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 app/views/nodes/trashed.html.erb (limited to 'app/views') diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 7933cba..319530d 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -68,5 +68,9 @@ <%= link_to menu_items_path, class: "action_button" do %> <%= icon("menu-2", library: "tabler", "aria-hidden": true) %> Navigation <% end %> + <% trash_count = Node.trash.children.count %> + <%= link_to trashed_nodes_path, class: "action_button" do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Trash<%= " (#{trash_count})" if trash_count > 0 %> + <% end %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index ae25571..4d4f6d5 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -46,7 +46,7 @@ <% end %> <% unless locked_by_other %> - <% if @node.draft && !@node.autosave %> + <% if @node.draft && !@node.autosave && !@node.in_trash? && !@node.trash_node? %>
<%= button_to 'Publish', publish_node_path(@node), method: :put, form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %> @@ -62,6 +62,15 @@ <% end %>
<% end %> + <% unless @node.trash_node? || @node.in_trash? || @node.root? %> +
+ <%= button_to trash_node_path(@node), method: :put, + form: { data: { confirm: "Move this page and everything beneath it to the Trash? All published content in it will go offline." }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> + Move to Trash + <% end %> +
+ <% end %> <% end %> @@ -70,6 +79,48 @@ <% end %> + <% if @node.in_trash? %> +
Trash
+
+
+ <% if (entry = @node.last_trash_entry) && entry.metadata.dig("path", "from") %> +
+ Was at + <%= entry.metadata.dig("path", "from") %> +
+ <% end %> +
+ Restore to + <% suggestion = @node.suggested_restore_parent %> + <%= form_tag restore_from_trash_node_path(@node), :method => :put, :class => "aligned_action_row" do %> +
+ <%= text_field_tag :restore_search_term, + suggestion && (suggestion.head&.title || suggestion.draft&.title || suggestion.slug), + :placeholder => "Search for a new parent…" %> +
+
+ <%= hidden_field_tag :parent_id, suggestion&.id %> + <%= submit_tag "Restore here", :class => "action_button action_button_no_margin_top" %> + <% end %> + Restored pages come back unpublished. Republish each page deliberately. +
+
+
+
+ <% doomed_below = @node.descendants.count %> + <%= button_to node_path(@node), method: :delete, + form: { data: { confirm: doomed_below > 0 ? + "Delete this page and the #{doomed_below} pages beneath it permanently? This cannot be undone." : + "Delete this page permanently? This cannot be undone." }, + class: 'button_to destructive' } do %> + <%= icon("trash-x", library: "tabler", "aria-hidden": true) %> + Delete permanently<%= " (including #{doomed_below} beneath)" if doomed_below > 0 %> + <% end %> +
+
+
+ <% end %> +
Translations
diff --git a/app/views/nodes/trashed.html.erb b/app/views/nodes/trashed.html.erb new file mode 100644 index 0000000..9a4808e --- /dev/null +++ b/app/views/nodes/trashed.html.erb @@ -0,0 +1,42 @@ +

Trash

+ +<% if @nodes.empty? %> +

The Trash is empty.

+<% else %> + <%= will_paginate @nodes %> + + + + + + + + <% @nodes.each do |node| %> + <% entry = node.last_trash_entry %> + + <% doomed_below = node.descendants.count %> + + + + + + <% end %> +
TitleWas atTrashedActions
+ <%= link_to (node.draft&.title || node.slug), node_path(node) %><%= " (+ #{doomed_below} beneath)" if doomed_below > 0 %> + <%= entry&.metadata&.dig("path", "from") %> + <% if entry %> + <%= entry.occurred_at.strftime("%Y-%m-%d %H:%M") %> by <%= entry.actor_name %> + <% end %> + + <%= button_to node_path(node), method: :delete, + form: { data: { confirm: doomed_below > 0 ? + "Delete \"#{node.draft&.title || node.slug}\" and the #{doomed_below} pages beneath it permanently? This cannot be undone." : + "Delete \"#{node.draft&.title || node.slug}\" permanently? This cannot be undone." }, + class: 'button_to destructive' } do %> + <%= icon("trash-x", library: "tabler", "aria-hidden": true) %> + Delete permanently + <% end %> +
+ <%= will_paginate @nodes %> +

To restore a page, open it and pick a new parent in its Trash section.

+<% end %> -- cgit v1.3