diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 02:15:29 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 02:15:29 +0200 |
| commit | d41ee504caedbe858e24ab2a23c7a804454c64c8 (patch) | |
| tree | eb3ec7c8f44d15fea40292d0a9e7358e41640edc /app/views/nodes | |
| parent | 5dced8b4b624aabf4215ba21b13957080345c326 (diff) | |
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.
Diffstat (limited to 'app/views/nodes')
| -rw-r--r-- | app/views/nodes/show.html.erb | 53 | ||||
| -rw-r--r-- | app/views/nodes/trashed.html.erb | 42 |
2 files changed, 94 insertions, 1 deletions
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 @@ | |||
| 46 | <% end %> | 46 | <% end %> |
| 47 | 47 | ||
| 48 | <% unless locked_by_other %> | 48 | <% unless locked_by_other %> |
| 49 | <% if @node.draft && !@node.autosave %> | 49 | <% if @node.draft && !@node.autosave && !@node.in_trash? && !@node.trash_node? %> |
| 50 | <div class="node_info_item"> | 50 | <div class="node_info_item"> |
| 51 | <%= button_to 'Publish', publish_node_path(@node), method: :put, | 51 | <%= button_to 'Publish', publish_node_path(@node), method: :put, |
| 52 | form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %> | 52 | form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %> |
| @@ -62,6 +62,15 @@ | |||
| 62 | <% end %> | 62 | <% end %> |
| 63 | </div> | 63 | </div> |
| 64 | <% end %> | 64 | <% end %> |
| 65 | <% unless @node.trash_node? || @node.in_trash? || @node.root? %> | ||
| 66 | <div class="node_info_item"> | ||
| 67 | <%= button_to trash_node_path(@node), method: :put, | ||
| 68 | 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 %> | ||
| 69 | <%= icon("trash", library: "tabler", "aria-hidden": true) %> | ||
| 70 | Move to Trash | ||
| 71 | <% end %> | ||
| 72 | </div> | ||
| 73 | <% end %> | ||
| 65 | <% end %> | 74 | <% end %> |
| 66 | </div> | 75 | </div> |
| 67 | 76 | ||
| @@ -70,6 +79,48 @@ | |||
| 70 | <% end %> | 79 | <% end %> |
| 71 | </div> | 80 | </div> |
| 72 | 81 | ||
| 82 | <% if @node.in_trash? %> | ||
| 83 | <div class="node_description">Trash</div> | ||
| 84 | <div class="node_content node_info_group"> | ||
| 85 | <div class="node_info_group_items"> | ||
| 86 | <% if (entry = @node.last_trash_entry) && entry.metadata.dig("path", "from") %> | ||
| 87 | <div class="node_info_item"> | ||
| 88 | <span class="node_info_label">Was at</span> | ||
| 89 | <%= entry.metadata.dig("path", "from") %> | ||
| 90 | </div> | ||
| 91 | <% end %> | ||
| 92 | <div class="node_info_item"> | ||
| 93 | <span class="node_info_label">Restore to</span> | ||
| 94 | <% suggestion = @node.suggested_restore_parent %> | ||
| 95 | <%= form_tag restore_from_trash_node_path(@node), :method => :put, :class => "aligned_action_row" do %> | ||
| 96 | <div class="restore_picker"> | ||
| 97 | <%= text_field_tag :restore_search_term, | ||
| 98 | suggestion && (suggestion.head&.title || suggestion.draft&.title || suggestion.slug), | ||
| 99 | :placeholder => "Search for a new parent…" %> | ||
| 100 | <div id="restore_search_results" class="search_results"></div> | ||
| 101 | </div> | ||
| 102 | <%= hidden_field_tag :parent_id, suggestion&.id %> | ||
| 103 | <%= submit_tag "Restore here", :class => "action_button action_button_no_margin_top" %> | ||
| 104 | <% end %> | ||
| 105 | <span class="field_hint">Restored pages come back unpublished. Republish each page deliberately.</span> | ||
| 106 | </div> | ||
| 107 | </div> | ||
| 108 | <div class="node_info_group_items"> | ||
| 109 | <div class="node_info_item"> | ||
| 110 | <% doomed_below = @node.descendants.count %> | ||
| 111 | <%= button_to node_path(@node), method: :delete, | ||
| 112 | form: { data: { confirm: doomed_below > 0 ? | ||
| 113 | "Delete this page and the #{doomed_below} pages beneath it permanently? This cannot be undone." : | ||
| 114 | "Delete this page permanently? This cannot be undone." }, | ||
| 115 | class: 'button_to destructive' } do %> | ||
| 116 | <%= icon("trash-x", library: "tabler", "aria-hidden": true) %> | ||
| 117 | Delete permanently<%= " (including #{doomed_below} beneath)" if doomed_below > 0 %> | ||
| 118 | <% end %> | ||
| 119 | </div> | ||
| 120 | </div> | ||
| 121 | </div> | ||
| 122 | <% end %> | ||
| 123 | |||
| 73 | <div class="node_description">Translations</div> | 124 | <div class="node_description">Translations</div> |
| 74 | <div class="node_content node_info_group"> | 125 | <div class="node_content node_info_group"> |
| 75 | <div class="node_info_group_items"> | 126 | <div class="node_info_group_items"> |
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 @@ | |||
| 1 | <h1>Trash</h1> | ||
| 2 | |||
| 3 | <% if @nodes.empty? %> | ||
| 4 | <p class="field_hint">The Trash is empty.</p> | ||
| 5 | <% else %> | ||
| 6 | <%= will_paginate @nodes %> | ||
| 7 | <table id="trashed_node_list"> | ||
| 8 | <tr> | ||
| 9 | <th>Title</th> | ||
| 10 | <th>Was at</th> | ||
| 11 | <th>Trashed</th> | ||
| 12 | <th>Actions</th> | ||
| 13 | </tr> | ||
| 14 | <% @nodes.each do |node| %> | ||
| 15 | <% entry = node.last_trash_entry %> | ||
| 16 | <tr> | ||
| 17 | <% doomed_below = node.descendants.count %> | ||
| 18 | <td> | ||
| 19 | <%= link_to (node.draft&.title || node.slug), node_path(node) %><%= " (+ #{doomed_below} beneath)" if doomed_below > 0 %> | ||
| 20 | </td> | ||
| 21 | <td><%= entry&.metadata&.dig("path", "from") %></td> | ||
| 22 | <td> | ||
| 23 | <% if entry %> | ||
| 24 | <%= entry.occurred_at.strftime("%Y-%m-%d %H:%M") %> by <%= entry.actor_name %> | ||
| 25 | <% end %> | ||
| 26 | </td> | ||
| 27 | <td> | ||
| 28 | <%= button_to node_path(node), method: :delete, | ||
| 29 | form: { data: { confirm: doomed_below > 0 ? | ||
| 30 | "Delete \"#{node.draft&.title || node.slug}\" and the #{doomed_below} pages beneath it permanently? This cannot be undone." : | ||
| 31 | "Delete \"#{node.draft&.title || node.slug}\" permanently? This cannot be undone." }, | ||
| 32 | class: 'button_to destructive' } do %> | ||
| 33 | <%= icon("trash-x", library: "tabler", "aria-hidden": true) %> | ||
| 34 | Delete permanently | ||
| 35 | <% end %> | ||
| 36 | </td> | ||
| 37 | </tr> | ||
| 38 | <% end %> | ||
| 39 | </table> | ||
| 40 | <%= will_paginate @nodes %> | ||
| 41 | <p class="field_hint">To restore a page, open it and pick a new parent in its Trash section.</p> | ||
| 42 | <% end %> | ||
