+
+
+ Head
+ <%= @node.head ? "#{@node.head.title} (rev #{@node.head.revision}, #{@node.head.updated_at})" : "none — never published" %>
+
+
+ Draft
+ <%= @node.draft ? "#{@node.draft.title} (rev #{@node.draft.revision}, saved #{@node.draft.updated_at})" : "none" %>
+
+
+ Autosave
+ <%= @node.autosave ? "#{@node.autosave.title} (unsaved, #{@node.autosave.updated_at})" : "none" %>
+
+
+
+ <% edit_label = @node.autosave ? "Continue Editing" : (@node.draft ? "Edit Draft" : "Edit") %>
+
+
+ <% if locked_by_other %>
+ <%= edit_label %>
+ <% else %>
+ <%= link_to (@node.autosave ? "Continue Editing" : (@node.draft ? "Edit Draft" : "Lock + Edit")), edit_node_path(@node), class: "action_button" %>
+ <% if !@node.draft && !@node.autosave %>
+ Nothing pending — this will start a fresh draft.
+ <% end %>
+ <% end %>
+
+
+ <% unless locked_by_other %>
+ <% if @node.draft && !@node.autosave %>
+
+ <%= button_to 'Publish', publish_node_path(@node), method: :put,
+ form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %>
+
+ <% end %>
+ <% if @node.draft || @node.autosave %>
+
+ <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'),
+ revert_node_path(@node), method: :put,
+ form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
+
+ <% end %>
+ <% end %>
+
+
+ <% if locked_by_other %>
+
Locked — see People below to unlock before editing, publishing, or discarding.
+ <% end %>
+
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index ceacf17..a6e8bf6 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -79,6 +79,9 @@ input[type=radio] {
#wrapper {
margin: 0 125px;
}
+ .node_action_bar.node_action_bar_save {
+ margin-left: 120px;
+ }
}
@media(max-width:1015px) {
#wrapper {
@@ -165,12 +168,14 @@ input[type=radio] {
margin-bottom: 40px;
}
-#node_action_bar {
- margin-bottom: 1rem;
-}
-
-#node_action_bar > * {
- margin-right: 0.5rem;
+.node_action_bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+ margin: 1rem 0 1.5rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #e8e8e8;
}
/* ============================================================
@@ -213,6 +218,18 @@ span.warning a {
text-decoration: underline;
}
+.error_messages {
+ border-left: 3px solid #cc0000;
+ background-color: #fdecea;
+ padding: 6px 10px;
+ margin-bottom: 1rem;
+}
+
+.error_messages ul {
+ margin: 0;
+ padding-left: 1.25rem;
+}
+
/* ============================================================
Pagination
============================================================ */
@@ -487,6 +504,16 @@ table.user_table td.user_login {
padding: 0.5rem 0.75rem;
}
+.node_info_group .disabled_action {
+ display: inline-block;
+ border: 1px solid #c0c0c0;
+ border-radius: 2px;
+ padding: 4px 12px;
+ font-weight: bold;
+ color: #969696;
+ cursor: not-allowed;
+}
+
.node_info_group_items {
display: flex;
flex-wrap: wrap;
@@ -514,6 +541,38 @@ table.user_table td.user_login {
margin-bottom: 0.25rem;
}
+.node_content.node_status {
+ border: 2px solid #000000;
+ background-color: #fafafa;
+}
+
+.node_status .node_info_group_items:first-child {
+ margin-bottom: 0.75rem;
+}
+
+.node_status .node_info_group_items + .node_info_group_items {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.node_status .field_hint {
+ margin-top: 0.5rem;
+ display: block;
+}
+
+#page_editor a.action_button,
+.node_status form.button_to input[type="submit"],
+.node_status form.button_to button[type="submit"] {
+ padding: 4px 12px;
+ line-height: 1.2;
+ box-sizing: border-box;
+}
+
+.node_status form.button_to input[type="submit"],
+.node_status form.button_to button[type="submit"] {
+ border: 1px solid transparent;
+}
+
.field_hint {
display: block;
margin-top: 2px;
@@ -562,6 +621,23 @@ div#page_editor {
margin-left: 10px;
}
+#page_editor a.action_button {
+ display: inline-block;
+ -webkit-appearance: none;
+ appearance: none;
+ border: 1px solid #000000;
+ border-radius: 2px;
+ padding: 4px 12px;
+ font-weight: bold;
+ text-decoration: none;
+ color: #000000;
+}
+
+#page_editor a.action_button:hover {
+ color: #ffffff;
+ background-color: #000000;
+}
+
@media(min-width:1016px) {
input#tag_list,
input#node_staged_slug,
--
cgit v1.3
From 205e6216fc7850fe717122c189e5003d1f9e8afe Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Fri, 10 Jul 2026 02:03:19 +0200
Subject: Add head/draft/autosave layer comparison at three UI entry points
Node#resolve_page_reference and #available_layer_pairs let
Page#diff_against compare named layers (head/draft/autosave), not
just numbered revisions -- autosave was never part of Node#pages, so
this was the missing piece.
Wired into nodes#show's Status section, nodes#edit right after an
autosave gets resurrected ("What changed?"), and the admin wizard's
current-drafts table, which now also lists autosave-only nodes it
previously never showed.
revisions#diff hides the numbered-revision picker when comparing
named layers (it can't represent them), shows a plain label instead,
and offers buttons to switch between whichever other pairs make
sense for the node's current state. Destroying the topmost layer is
available directly from the diff view, reusing the existing revert!
path.
"Discard changes" is renamed "Discard Autosave" everywhere it
appears, to match "Destroy Draft".
---
app/controllers/admin_controller.rb | 4 +--
app/controllers/nodes_controller.rb | 2 +-
app/controllers/revisions_controller.rb | 16 ++++++++---
app/helpers/revisions_helper.rb | 5 ++++
app/models/node.rb | 21 ++++++++++++++
app/views/admin/index.html.erb | 13 +++++++--
app/views/nodes/edit.html.erb | 9 +++++-
app/views/nodes/show.html.erb | 12 +++++++-
app/views/revisions/diff.html.erb | 40 +++++++++++++++++++++++----
config/routes.rb | 1 +
test/controllers/admin_controller_test.rb | 15 ++++++++--
test/controllers/revisions_controller_test.rb | 39 ++++++++++++++++++++++++++
test/models/node_test.rb | 22 +++++++++++++++
13 files changed, 179 insertions(+), 20 deletions(-)
(limited to 'app/views/nodes/show.html.erb')
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
index 3fa0519..6ab2135 100644
--- a/app/controllers/admin_controller.rb
+++ b/app/controllers/admin_controller.rb
@@ -5,10 +5,10 @@ class AdminController < ApplicationController
before_action :login_required
def index
- @drafts = Node.where("draft_id IS NOT NULL")
+ @drafts = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL")
.limit(50).order("updated_at desc")
- @drafts_count = Node.where("draft_id IS NOT NULL").count
+ @drafts_count = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL").count
@recent_changes = Node.where(
"updated_at < ? AND updated_at > ? AND parent_id IS NOT NULL",
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index 38d42d9..d1538e1 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -72,7 +72,7 @@ class NodesController < ApplicationController
if @node.autosave
flash.now[:notice] =
"This page has unsaved changes from a previous session, shown below. " \
- "Save to keep them, or use \"Discard changes\" below to go back to the last saved version."
+ "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version."
elsif freshly_locked
flash.now[:notice] = "Node locked and ready to edit"
end
diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb
index 9acb26f..4b0c549 100644
--- a/app/controllers/revisions_controller.rb
+++ b/app/controllers/revisions_controller.rb
@@ -21,10 +21,18 @@ class RevisionsController < ApplicationController
params[:start_revision], params[:end_revision] = 1, 1
end
- @start = @node.pages.find_by_revision( params[:start_revision] )
- @end = @node.pages.find_by_revision( params[:end_revision] )
- @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline
- @diff = @end.diff_against( @start, view: @diff_view )
+ @start = @node.resolve_page_reference(params[:start_revision])
+ @end = @node.resolve_page_reference(params[:end_revision])
+
+ if @start.nil? || @end.nil?
+ flash[:error] = "That comparison is no longer available."
+ redirect_to(node_path(@node)) and return
+ end
+
+ @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline
+ @diff = @end.diff_against(@start, view: @diff_view)
+ @available_layer_pairs = @node.available_layer_pairs
+ @locked_by_other = @node.locked? && @node.lock_owner != current_user
end
def show
diff --git a/app/helpers/revisions_helper.rb b/app/helpers/revisions_helper.rb
index fdb51f8..a629013 100644
--- a/app/helpers/revisions_helper.rb
+++ b/app/helpers/revisions_helper.rb
@@ -1,2 +1,7 @@
module RevisionsHelper
+ # Human-readable label for a diff endpoint -- "head"/"draft"/"autosave"
+ # get their name; anything else is a revision number.
+ def describe_page_reference(ref)
+ %w[head draft autosave].include?(ref.to_s) ? ref.to_s.capitalize : "revision #{ref}"
+ end
end
diff --git a/app/models/node.rb b/app/models/node.rb
index 7675ab6..82d9954 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -123,6 +123,27 @@ class Node < ApplicationRecord
self.draft.reload
end
+ def resolve_page_reference ref
+ case ref.to_s
+ when "head" then head
+ when "draft" then draft
+ when "autosave" then autosave
+ else pages.find_by_revision(ref)
+ end
+ end
+
+ # Which layer-pairs are meaningful to compare right now, given this
+ # node's actual state. Head vs autosave only shows up when no draft
+ # sits between them -- with a draft present, autosave is compared
+ # against the draft, never past it straight to head.
+ def available_layer_pairs
+ pairs = []
+ pairs << [:head, :draft] if head && draft
+ pairs << [:draft, :autosave] if draft && autosave
+ pairs << [:head, :autosave] if head && autosave && !draft
+ pairs
+ end
+
def find_or_create_draft current_user
self.wipe_draft!
if draft && self.lock_owner == current_user
diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb
index 77b45f4..c67ccb3 100644
--- a/app/views/admin/index.html.erb
+++ b/app/views/admin/index.html.erb
@@ -82,9 +82,9 @@
-
-
Current Drafts (<%= @drafts_count %>)
-
+
+
Current Drafts & Autosaves (<%= @drafts_count %>)
+
<% @drafts.each do |node| %>
">
@@ -103,6 +104,9 @@
|
<%= link_to 'show', node_path(node) %>
<%= link_to 'Revisions', node_revisions_path(node) %>
+ <% if pair = node.available_layer_pairs.last %>
+ <%= link_to 'diff', diff_node_revisions_path(node, start_revision: pair.first, end_revision: pair.last) %>
+ <% end %>
|
<%= node.lock_owner.login if node.lock_owner %>
@@ -110,6 +114,9 @@
|
<%= link_to ( node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY" ) ), node_revisions_path(node) %>
|
+
+ <%= node.autosave ? "unsaved changes" : "" %>
+ |
<% end %>
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index cdc9b36..1c19410 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -6,9 +6,16 @@
disabled: @node.autosave.present? %>
<% if @node.autosave || (@node.draft && @node.head) %>
- <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'),
+ <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
+ <% if pair = @node.available_layer_pairs.find { |p| p.include?(:autosave) } %>
+ <%= button_to 'What changed?',
+ diff_node_revisions_path(@node),
+ method: :get,
+ params: { start_revision: pair.first, end_revision: pair.last },
+ form: { class: 'button_to computation' } %>
+ <% end %>
<% end %>
<%= submit_tag "Save Draft", form: dom_id(@node, :edit) %>
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 036caf2..2469310 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -32,6 +32,16 @@
<% end %>
+ <% @node.available_layer_pairs.each do |pair| %>
+
+ <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}",
+ diff_node_revisions_path(@node),
+ method: :get,
+ params: { start_revision: pair.first, end_revision: pair.last },
+ form: { class: 'button_to computation' } %>
+
+ <% end %>
+
<% unless locked_by_other %>
<% if @node.draft && !@node.autosave %>
@@ -41,7 +51,7 @@
<% end %>
<% if @node.draft || @node.autosave %>
- <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'),
+ <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb
index d7bb528..3157dca 100644
--- a/app/views/revisions/diff.html.erb
+++ b/app/views/revisions/diff.html.erb
@@ -4,11 +4,41 @@
<%= link_to 'Revisions', node_revisions_path(@node) %>
-<%= form_tag diff_node_revisions_path do %>
- <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %>
- <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %>
- <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %>
- <%= submit_tag 'Diff' %>
+
+ Comparing <%= describe_page_reference(params[:start_revision]) %>
+ against <%= describe_page_reference(params[:end_revision]) %>
+
+
+<% numeric_comparison = params[:start_revision].to_s =~ /\A\d+\z/ && params[:end_revision].to_s =~ /\A\d+\z/ %>
+
+<% if numeric_comparison %>
+ <%= form_tag diff_node_revisions_path do %>
+ <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %>
+ <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %>
+ <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %>
+ <%= submit_tag 'Diff' %>
+ <% end %>
+<% else %>
+
<%= link_to 'Compare two numbered revisions instead', node_revisions_path(@node) %>
+<% end %>
+
+<% if @available_layer_pairs.present? %>
+
+ <% @available_layer_pairs.each do |pair| %>
+ <% next if [params[:start_revision].to_s, params[:end_revision].to_s].sort == pair.map(&:to_s).sort %>
+ <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}",
+ diff_node_revisions_path(@node),
+ method: :get,
+ params: { start_revision: pair.first, end_revision: pair.last, view: @diff_view },
+ form: { class: 'button_to computation' } %>
+ <% end %>
+
+ <% if !@locked_by_other && (@node.autosave || @node.draft) %>
+ <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
+ revert_node_path(@node), method: :put,
+ form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
+ <% end %>
+
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index c0aef2f..da6b626 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -47,6 +47,7 @@ Cccms::Application.routes.draw do
resources :revisions do
collection do
post :diff
+ get :diff
end
member do
put :restore
diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb
index 9bbf29b..d6005ba 100644
--- a/test/controllers/admin_controller_test.rb
+++ b/test/controllers/admin_controller_test.rb
@@ -1,8 +1,17 @@
require 'test_helper'
class AdminControllerTest < ActionController::TestCase
- # Replace this with your real tests.
- test "the truth" do
- assert true
+ test "current drafts includes nodes with only an autosave" do
+ node = Node.root.children.create!(:slug => "admin_autosave_only")
+ node.lock_for_editing!(User.find_by_login("aaron"))
+ node.autosave!({title: "in progress"}, User.find_by_login("aaron"))
+ node.save_draft!(User.find_by_login("aaron"))
+ node.publish_draft!
+ node.lock_for_editing!(User.find_by_login("aaron"))
+ node.autosave!({title: "editing again"}, User.find_by_login("aaron"))
+
+ login_as :quentin
+ get :index
+ assert_includes assigns(:drafts), node
end
end
diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb
index caca6bf..162e6f1 100644
--- a/test/controllers/revisions_controller_test.rb
+++ b/test/controllers/revisions_controller_test.rb
@@ -100,4 +100,43 @@ class RevisionsControllerTest < ActionController::TestCase
assert_response :success
assert_select ".diff_column", 2
end
+
+ test "diffing head against draft by name" do
+ login_as :quentin
+ @node.find_or_create_draft(@user)
+ @node.draft.update(:body => "draft body")
+
+ post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" })
+ assert_response :success
+ end
+
+ test "diffing a layer pair that no longer exists redirects with a flash" do
+ login_as :quentin
+ post(:diff, params: { :node_id => @node.id, :start_revision => "draft", :end_revision => "autosave" })
+ assert_redirected_to node_path(@node)
+ assert flash[:error].present?
+ end
+
+ test "diffing by name shows a clear comparison label instead of a misleading revision picker" do
+ login_as :quentin
+ @node.find_or_create_draft(@user)
+
+ post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" })
+ assert_response :success
+ assert_select "strong", "Head"
+ assert_select "strong", "Draft"
+ assert_select "select[name=?]", "start_revision", :count => 0
+ end
+
+ test "pair-switcher buttons carry their params as real hidden fields, not a query string" do
+ login_as :quentin
+ @node.find_or_create_draft(@user)
+ @node.lock_for_editing!(@user)
+ @node.autosave!({ :body => "unsaved" }, @user)
+
+ post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" })
+ assert_response :success
+ assert_select "form.computation input[type=hidden][name=start_revision]"
+ assert_select "form.computation input[type=hidden][name=end_revision]"
+ end
end
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 2138c19..9e71dec 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -473,4 +473,26 @@ class NodeTest < ActiveSupport::TestCase
node.publish_draft!
end
end
+
+ test "available_layer_pairs matches the six-state table" do
+ node = Node.root.children.create!(:slug => "layer_pairs_test")
+ user = @user1 || User.find_by_login("aaron")
+
+ assert_equal [[:draft, :autosave]], (node.lock_for_editing!(user); node.autosave!({title: "v1"}, user); node.available_layer_pairs) # state F
+
+ node.save_draft!(user)
+ node.publish_draft!
+ assert_equal [], node.available_layer_pairs # state A
+
+ node.lock_for_editing!(user)
+ node.autosave!({title: "v2"}, user)
+ assert_equal [[:head, :autosave]], node.available_layer_pairs # state B
+
+ node.save_draft!(user)
+ assert_equal [[:head, :draft]], node.available_layer_pairs # state C
+
+ node.lock_for_editing!(user)
+ node.autosave!({title: "v3"}, user)
+ assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D
+ end
end
--
cgit v1.3
From c2b2648d327e1c1749c37fe2e58cd051ed871547 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Fri, 10 Jul 2026 02:13:02 +0200
Subject: Destroying Draft or Discarding Autosave drops you where you left
---
app/controllers/nodes_controller.rb | 5 +++-
app/views/nodes/show.html.erb | 1 +
app/views/revisions/diff.html.erb | 1 +
test/controllers/nodes_controller_test.rb | 39 +++++++++++++++++++++++++++++++
4 files changed, 45 insertions(+), 1 deletion(-)
(limited to 'app/views/nodes/show.html.erb')
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index d1538e1..6fcd930 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -128,7 +128,10 @@ class NodesController < ApplicationController
def revert
@node.lock_for_editing!(current_user)
@node.revert!(current_user)
- if @node.draft
+
+ if params[:return_to].present?
+ redirect_to safe_return_to(params[:return_to])
+ elsif @node.draft
redirect_to edit_node_path(@node)
else
redirect_to node_path(@node)
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 2469310..8b9e98b 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -53,6 +53,7 @@
<%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
+ params: { return_to: request.path },
form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
<% end %>
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb
index 3157dca..490cf17 100644
--- a/app/views/revisions/diff.html.erb
+++ b/app/views/revisions/diff.html.erb
@@ -36,6 +36,7 @@
<% if !@locked_by_other && (@node.autosave || @node.draft) %>
<%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
+ params: { return_to: request.fullpath },
form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
<% end %>
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index f3e04c2..ce3419c 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -423,4 +423,43 @@ class NodesControllerTest < ActionController::TestCase
assert_select "form.button_to.destructive", count: 0
end
+ test "reverting from nodes#show returns to the show page, not the editor, even if a draft remains" do
+ user = User.find_by_login("aaron")
+ node = Node.root.children.create!(:slug => "revert_return_to_test")
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v1"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v2"}, user)
+ node.save_draft!(user)
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v3"}, user)
+ # state D: head, draft, and autosave all present, locked by aaron
+
+ login_as :aaron
+ put :revert, params: { :id => node.id, :return_to => node_path(node) }
+ assert_redirected_to node_path(node)
+ node.reload
+ assert node.draft.present?
+ assert node.autosave.blank?
+ end
+
+ test "reverting from nodes#edit without return_to still lands back in the editor when a draft remains" do
+ user = User.find_by_login("aaron")
+ node = Node.root.children.create!(:slug => "revert_default_test")
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v1"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v2"}, user)
+ node.save_draft!(user)
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v3"}, user)
+
+ login_as :aaron
+ put :revert, params: { :id => node.id }
+ assert_redirected_to edit_node_path(node)
+ end
end
--
cgit v1.3
From b928656088ef14e7d004de726df0d1cd70d1cdba Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Fri, 10 Jul 2026 03:19:47 +0200
Subject: Move link to full revision list out of summary
---
app/views/nodes/show.html.erb | 2 +-
public/stylesheets/admin.css | 14 +++++++++++---
2 files changed, 12 insertions(+), 4 deletions(-)
(limited to 'app/views/nodes/show.html.erb')
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 8b9e98b..5756649 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -132,7 +132,6 @@
<%= pluralize(@node.pages.count, 'revision', 'revisions') %>
- · <%= link_to 'full history (diff / restore)', node_revisions_path(@node) %>
<% @node.pages.order(:revision).each do |page| %>
@@ -140,6 +139,7 @@
<% end %>
+ <%= link_to 'Full history (diff / restore)', node_revisions_path(@node) %>
Tags
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 1bb6cf4..e4bdac4 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -65,6 +65,14 @@ select {
border: 1px solid #989898;
}
+summary {
+ cursor: pointer;
+}
+
+summary:hover {
+ background-color: #f7f7f7;
+}
+
input[type=password] {
border: 1px solid #989898;
}
@@ -665,9 +673,9 @@ form.button_to button[type="submit"] {
margin-top: 0;
}
-/* Identical declaration block to #search_results p span.result_path /
- #menu_search_results p span.result_path below -- three independent
- copies of the same "small gray helper text" style. */
+.revisions_full_history_link {
+ margin-bottom: 0;
+}
/* Layout only -- the at-rest visibility (wavy underline) for these
links comes from the scoped rule in Base elements above. */
--
cgit v1.3
From 15aff3eff51809d1ce21caab406a0ef8b13624b6 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 11 Jul 2026 01:33:07 +0200
Subject: Do not offer to destroy the only draft of a never-published node
---
app/views/nodes/show.html.erb | 2 +-
test/controllers/nodes_controller_test.rb | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
(limited to 'app/views/nodes/show.html.erb')
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 5756649..07cb5d1 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -49,7 +49,7 @@
form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %>
<% end %>
- <% if @node.draft || @node.autosave %>
+ <% if @node.autosave || (@node.draft && @node.head) %>
<%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 37091d5..b563d4d 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -463,4 +463,12 @@ class NodesControllerTest < ActionController::TestCase
put :revert, params: { :id => node.id }
assert_redirected_to edit_node_path(node)
end
+
+ test "nodes#show does not offer to destroy the only draft of a never-published node" do
+ node = Node.root.children.create!(:slug => "draft_only_test")
+ login_as :quentin
+ get :show, params: { :id => node.id }
+ assert_response :success
+ assert_select "form.destructive", :count => 0
+ end
end
--
cgit v1.3
From 848ce18303f927bfadaf0965a769cae2c62492a0 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sun, 12 Jul 2026 23:43:02 +0200
Subject: Normalize action-button wording and add icons app-wide
"New X" becomes "Create X" throughout (users, events, assets, menu
items, nodes), matching the verb-first pattern the dashboard's own
signposts already established, with a shared plus icon rather than a
document-flavored one that only made sense next to "post". The
"Destroy"/"destroy"/"Delete" family is normalized to "Destroy"
everywhere, with a shared trash icon; occurrences#index and
pages#index also pick up the destructive button class they'd been
silently missing. Filter and Search convert from submit_tag to
button_tag, the only way either can hold an icon alongside its label.
Edit and the node editor's three dynamic labels (Continue Editing /
Edit Draft / Lock + Edit) share one icon without touching their
wording -- unlike Destroy's family, the state nuance in the text is
real information, not just inconsistent phrasing.
---
app/views/assets/index.html.erb | 8 ++++++--
app/views/assets/show.html.erb | 4 +++-
app/views/events/edit.html.erb | 6 ++++--
app/views/events/index.html.erb | 8 ++++++--
app/views/events/show.html.erb | 8 ++++++--
app/views/events/without_node.html.erb | 4 +++-
app/views/menu_items/index.html.erb | 11 +++++++----
app/views/nodes/_node_list.html.erb | 4 +++-
app/views/nodes/chapters.html.erb | 6 ++++--
app/views/nodes/edit.html.erb | 8 +++++---
app/views/nodes/index.html.erb | 4 +++-
app/views/nodes/show.html.erb | 15 ++++++++++-----
app/views/occurrences/index.html.erb | 4 +++-
app/views/pages/index.html.erb | 4 +++-
app/views/revisions/diff.html.erb | 10 ++++++----
app/views/users/_user.html.erb | 7 ++++---
app/views/users/index.html.erb | 8 ++++++--
app/views/users/show.html.erb | 4 +++-
test/controllers/users_controller_test.rb | 2 +-
19 files changed, 86 insertions(+), 39 deletions(-)
(limited to 'app/views/nodes/show.html.erb')
diff --git a/app/views/assets/index.html.erb b/app/views/assets/index.html.erb
index 6591091..8c35561 100644
--- a/app/views/assets/index.html.erb
+++ b/app/views/assets/index.html.erb
@@ -1,6 +1,8 @@
Assets
-<%= link_to 'New asset', new_asset_path, class: 'action_button' %>
+<%= link_to new_asset_path, class: 'action_button' do %>
+ <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create asset
+<% end %>
<%= will_paginate @assets %>
@@ -20,7 +22,9 @@
<%= asset.upload.content_type %> |
<%= link_to 'Show', asset %> |
<%= link_to 'Edit', edit_asset_path(asset) %> |
- <%= button_to 'Destroy', asset, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } %> |
+ <%= button_to asset, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } do %>
+ <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy
+ <% end %> |
<% end %>