From 95955abaa339098755a214cfcadf87c90211fe64 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 1 Jul 2026 00:24:10 +0200 Subject: Add RRULE humanizer and wire events into nodes#show - app/models/concerns/rrule_humanizer.rb: new concern included into Event, renders recurring schedule as natural-language German or English from RRULE string; handles WEEKLY/MONTHLY, biweekly (INTERVAL=2), ordinal weekday positions (1TU, -1TH, -2WE), BYMONTH single-month exclusions (December pause convention); gracefully returns nil for COUNT/UNTIL/unrecognized shapes - test/models/concerns/rrule_humanizer_test.rb: 15 tests covering all distinct RRULE shapes found in production data - app/helpers/nodes_helper.rb: add event_schedule_text helper combining humanize_rrule with start_time formatting - app/views/nodes/show.html.erb: add events row, conditionally rendered when node has associated events - config/locales/de.yml, en.yml: add event_schedule_time, event_schedule_unrecognized, event_schedule_none keys --- app/views/nodes/show.html.erb | 14 +++++++++++++- 1 file changed, 13 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 de4d8a26..72232194 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -42,6 +42,18 @@ Tagged with: <%= @page.tag_list %> + <% if @node.events.any? %> + + Events + + + + + <% end %> Title <%= sanitize( @page.title ) %> @@ -59,4 +71,4 @@ - \ No newline at end of file + -- cgit v1.3 From b6416e86c9c58b8e886c14de55b01aeb863b8676 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 2 Jul 2026 00:12:02 +0200 Subject: Clean up events views: return_to, subnav, remove custom_rrule - events_controller: wire return_to through show and new actions; create respects return_to with fallback to node/event path; new pre-populates node_id and tag_list from params - events/edit: remove custom_rrule checkbox; node link removed from subnav (use show for node navigation); destroy button added - events/new: remove custom_rrule checkbox; add return_to hidden field and back link; tag_list field added - events/show: fix back link via safe_return_to; add node link to subnav; add destroy button; remove custom_rrule display; show humanized rrule below raw string - events/index: destructive class on destroy button; node_id column replaced with node link; show/edit links carry return_to - nodes/show: add show/edit links to event entries with return_to --- app/controllers/events_controller.rb | 10 +++++++--- app/views/events/edit.html.erb | 9 +++------ app/views/events/index.html.erb | 10 ++++------ app/views/events/new.html.erb | 7 ++++--- app/views/events/show.html.erb | 16 +++++++++++----- app/views/nodes/show.html.erb | 6 +++++- 6 files changed, 34 insertions(+), 24 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 3a60cf93..9bed5ddd 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -21,6 +21,7 @@ class EventsController < ApplicationController # GET /events/1.xml def show @event = Event.find(params[:id]) + @return_to = params[:return_to] || events_path respond_to do |format| format.html # show.html.erb @@ -31,7 +32,10 @@ class EventsController < ApplicationController # GET /events/new # GET /events/new.xml def new - @event = Event.new(:node_id => params[:node_id]) + @event = Event.new( + node_id: params[:node_id], + tag_list: params[:tag_list] + ) respond_to do |format| format.html # new.html.erb @@ -53,7 +57,7 @@ class EventsController < ApplicationController respond_to do |format| if @event.save flash[:notice] = 'Event was successfully created.' - format.html { redirect_to(@event.node ? edit_node_path(@event.node) : edit_event_path(@event)) } + format.html { redirect_to(safe_return_to(params[:return_to] || (@event.node ? edit_node_path(@event.node) : edit_event_path(@event)))) } format.xml { render :xml => @event, :status => :created, :location => @event } else format.html { render :action => "new" } @@ -94,6 +98,6 @@ class EventsController < ApplicationController private def event_params - params.require(:event).permit(:title, :description, :is_primary, :start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location) + params.require(:event).permit(:title, :description, :start_time, :end_time, :rrule, :allday, :url, :latitude, :longitude, :node_id, :location, :tag_list) end end diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb index 17457df4..5aee5019 100644 --- a/app/views/events/edit.html.erb +++ b/app/views/events/edit.html.erb @@ -1,9 +1,6 @@ <% content_for :subnavigation do %> <%= link_to 'back', safe_return_to(params[:return_to] || events_path) %> - <% if @event.node %> - <%= link_to 'node', edit_node_path(@event.node) %> - <% end %> - <%= link_to 'show', @event %> + <%= button_to 'destroy', event_path(@event), method: :delete, form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } %> <% end %>

Editing event

@@ -25,8 +22,8 @@ <%= f.text_field :rrule %>

- <%= f.label :custom_rrule %>
- <%= f.check_box :custom_rrule %> + <%= f.label :tag_list, "Tags" %>
+ <%= f.text_field :tag_list %>

<%= f.label :allday %>
diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 064fa860..d0458d71 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -3,7 +3,6 @@ - @@ -16,17 +15,16 @@ <% @events.each do |event| %> - - - - - + + + + <% end %>
TitleIs primary Start time End time Rrule
<%=h event.display_title %><%=h event.is_primary %> <%=h event.start_time %> <%=h event.end_time %> <%=h event.rrule %> <%=h event.custom_rrule %> <%=h event.allday %> <%=h event.url %><%=h event.node_id %><%= link_to 'Show', event %><%= link_to 'Edit', edit_event_path(event) %><%= button_to 'Destroy', event, method: :delete, form: { data: { confirm: 'Are you sure?' } } %><%= event.node ? link_to(event.node_id, node_path(event.node)) : '' %><%= link_to 'show', event %><%= link_to 'edit', edit_event_path(event) %><%= button_to 'destroy', event, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } %>
diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb index cd892c5d..4e1ef53e 100644 --- a/app/views/events/new.html.erb +++ b/app/views/events/new.html.erb @@ -16,8 +16,8 @@ <%= f.text_field :rrule %>

- <%= f.label :custom_rrule %>
- <%= f.check_box :custom_rrule %> + <%= f.label :tag_list, "Tags" %>
+ <%= f.text_field :tag_list %>

<%= f.label :allday %>
@@ -37,10 +37,11 @@

<%= f.hidden_field :node_id %> + <%= hidden_field_tag :return_to, params[:return_to] %>

<%= f.submit 'Create' %>

<% end %> -<%= link_to 'Back', events_path %> +<%= link_to 'Back', safe_return_to(params[:return_to] || events_path) %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index ba14a7d3..e206bc4c 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -1,6 +1,9 @@ <% content_for :subnavigation do %> - <%= link_to 'back', edit_node_path(@event.node) %> - <%= link_to 'edit', edit_event_path(@event) %> + <% if @event.node %> + <%= link_to 'node', node_path(@event.node) %> + <% end %> + <%= link_to 'edit', edit_event_path(@event, return_to: request.path) %> + <%= button_to 'destroy', event_path(@event), method: :delete, form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } %> <% end %>

Event for node <%= @event.node.unique_name %>

@@ -18,11 +21,14 @@

Rrule: <%=h @event.rrule %> + <% if (human = @event.humanize_rrule(I18n.locale)) %> +
( <%= human %> ) + <% end %>

- Custom rrule: - <%=h @event.custom_rrule %> + Tags: + <%=h @event.tag_list %>

@@ -43,4 +49,4 @@

Longitude: <%=h @event.longitude %> -

\ No newline at end of file +

diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 72232194..2ce3853e 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -48,7 +48,11 @@ -- cgit v1.3 From 206dc5c50a73c5402b90d7fdc8945d3ba9356758 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 4 Jul 2026 01:24:55 +0200 Subject: Add self-service event creation from nodes#show nodes#show's events table now renders unconditionally (previously hidden entirely for a zero-event node) and gains an "add event" link. The suggested tag_list is derived from the page's own category tags via NodesHelper::DEFAULT_EVENT_TAG_BY_PAGE_TAG (erfa-detail/ chaostreff-detail -> open-day) rather than hardcoded, and degrades to a blank field for any node whose tags don't match - deliberately universal, not chapter-specific, since Updates have historically carried event dates the same way. events#new surfaces *why* the tag was pre-filled via flash.now (not flash - this is a same-request render, not a redirect), using an explicit auto_tag_source param passed alongside tag_list rather than having the controller re-derive the reason from node_id. No destroy link added to nodes#show's events list - deliberate, per existing subnav-semantics convention (destructive actions live on the resource's own views). Covered directly by test. Adds real coverage for EventsController, previously 100% commented-out scaffold, plus unit tests for the new tag-mapping helper and the weekday-abbreviation helpers from the prior commit. --- app/controllers/events_controller.rb | 4 + app/helpers/nodes_helper.rb | 14 +++ app/views/nodes/show.html.erb | 4 +- test/controllers/events_controller_test.rb | 167 ++++++++++++++++++++------- test/controllers/nodes_controller_test.rb | 33 ++++++ test/models/concerns/rrule_humanizer_test.rb | 15 +++ test/models/helpers/content_helper_test.rb | 4 + test/models/helpers/nodes_helper_test.rb | 21 ++++ 8 files changed, 219 insertions(+), 43 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 9bed5ddd..b98a38e8 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -37,6 +37,10 @@ class EventsController < ApplicationController tag_list: params[:tag_list] ) + if params[:tag_list].present? && params[:auto_tag_source].present? + flash.now[:notice] = "Tag '#{params[:tag_list]}' was pre-filled because this page is tagged '#{params[:auto_tag_source]}'. You can remove it below." + end + respond_to do |format| format.html # new.html.erb format.xml { render :xml => @event } diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb index 329bcc53..093bfc6a 100644 --- a/app/helpers/nodes_helper.rb +++ b/app/helpers/nodes_helper.rb @@ -44,6 +44,20 @@ module NodesHelper ]) end + DEFAULT_EVENT_TAG_BY_PAGE_TAG = { + 'erfa-detail' => 'open-day', + 'chaostreff-detail' => 'open-day' + }.freeze + + def default_event_tag_mapping(page) + page_tags = page.tag_list + DEFAULT_EVENT_TAG_BY_PAGE_TAG.find { |page_tag, _| page_tags.include?(page_tag) } + end + + def default_event_tag_list(page) + default_event_tag_mapping(page)&.last + end + def event_schedule_text(event) if event.rrule.present? recurrence = event.humanize_rrule(I18n.locale) diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 2ce3853e..c533a55e 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -42,7 +42,6 @@ Tagged with: <%= @page.tag_list %> - <% if @node.events.any? %> Events @@ -55,9 +54,10 @@ <% end %> + <% mapping = default_event_tag_mapping(@page) %> + <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %> - <% end %> Title <%= sanitize( @page.title ) %> diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb index 14e534ea..9371ca78 100644 --- a/test/controllers/events_controller_test.rb +++ b/test/controllers/events_controller_test.rb @@ -1,45 +1,130 @@ require 'test_helper' class EventsControllerTest < ActionController::TestCase - # test "should get index" do - # get :index - # assert_response :success - # assert_not_nil assigns(:events) - # end - # - # test "should get new" do - # get :new - # assert_response :success - # end - # - # test "should create event" do - # assert_difference('Event.count') do - # post :create, params: { :event => { } } - # end - # - # assert_redirected_to event_path(assigns(:event)) - # end - # - # test "should show event" do - # get :show, params: { :id => events(:one).to_param } - # assert_response :success - # end - # - # test "should get edit" do - # get :edit, params: { :id => events(:one).to_param } - # assert_response :success - # end - # - # test "should update event" do - # put :update, params: { :id => events(:one).to_param, :event => { } } - # assert_redirected_to event_path(assigns(:event)) - # end - # - # test "should destroy event" do - # assert_difference('Event.count', -1) do - # delete :destroy, params: { :id => events(:one).to_param } - # end - # - # assert_redirected_to events_path - # end + + test "should get index" do + login_as :quentin + get :index + assert_response :success + assert_not_nil assigns(:events) + end + + test "should get new" do + login_as :quentin + get :new + assert_response :success + end + + test "new pre-fills tag_list and explains it via flash when auto_tag_source is given" do + login_as :quentin + node = create_node_with_published_page + + get :new, params: { node_id: node.id, tag_list: "open-day", auto_tag_source: "erfa-detail" } + + assert_response :success + assert_equal "open-day", assigns(:event).tag_list.to_s + assert_match "open-day", flash[:notice] + assert_match "erfa-detail", flash[:notice] + end + + test "new does not flash without an auto_tag_source" do + login_as :quentin + + get :new, params: { tag_list: "open-day" } + + assert_response :success + assert_nil flash[:notice] + end + + test "new with no params renders a blank, unflashed form" do + login_as :quentin + + get :new + + assert_response :success + assert_nil assigns(:event).tag_list.presence + assert_nil flash[:notice] + end + + test "should show event" do + login_as :quentin + node = create_node_with_published_page + event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) + + get :show, params: { id: event.id } + assert_response :success + end + + test "should get edit" do + login_as :quentin + node = create_node_with_published_page + event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) + + get :edit, params: { id: event.id } + assert_response :success + end + + test "should create event attached to a node" do + login_as :quentin + node = create_node_with_published_page + + assert_difference('Event.count') do + post :create, params: { + event: { + node_id: node.id, + start_time: Time.now, + end_time: Time.now + 1.hour, + tag_list: "open-day" + } + } + end + + assert_redirected_to edit_node_path(node) + assert_equal 'Event was successfully created.', flash[:notice] + end + + test "should not create an event without a title or a node_id" do + login_as :quentin + + assert_no_difference('Event.count') do + post :create, params: { event: { start_time: Time.now, end_time: Time.now + 1.hour } } + end + + assert_response :success # re-renders :new, not a redirect + end + + test "should honour return_to on create" do + login_as :quentin + node = create_node_with_published_page + + post :create, params: { + event: { node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour }, + return_to: node_path(node) + } + + assert_redirected_to node_path(node) + end + + test "should update event" do + login_as :quentin + node = create_node_with_published_page + event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) + + put :update, params: { id: event.id, event: { title: "Updated title" } } + + assert_redirected_to events_path + assert_equal "Updated title", event.reload.title + end + + test "should destroy event" do + login_as :quentin + node = create_node_with_published_page + event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) + + assert_difference('Event.count', -1) do + delete :destroy, params: { id: event.id } + end + + assert_redirected_to events_url + end end diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 53799f13..f14e27ce 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb @@ -379,4 +379,37 @@ class NodesControllerTest < ActionController::TestCase get :index end + test "show renders events row and add-link for zero-event chapter node" do + login_as :quentin + node = create_node_with_published_page + node.head.tag_list = "erfa-detail" + node.head.save! + + get :show, params: { id: node.id } + assert_response :success + assert_select "a", text: "add event" + assert_select "a[href*='tag_list=open-day']" + assert_select "a[href*='auto_tag_source=erfa-detail']" + end + + test "show renders events row without a tag default for untagged node" do + login_as :quentin + node = create_node_with_published_page + + get :show, params: { id: node.id } + assert_response :success + assert_select "a", text: "add event" + assert_select "a[href*='tag_list=']", count: 0 + end + + 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) + + get :show, params: { id: node.id } + assert_response :success + assert_select "form.button_to.destructive", count: 0 + end + end diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb index 500dbc7d..279ff73f 100644 --- a/test/models/concerns/rrule_humanizer_test.rb +++ b/test/models/concerns/rrule_humanizer_test.rb @@ -81,4 +81,19 @@ class RruleHumanizerTest < ActiveSupport::TestCase test "falls back to english for unknown locale" do assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :fr) end + + test "wday_abbr returns the correct German abbreviation for each day" do + monday = Time.parse("2026-07-06") # confirmed Monday + assert_equal "Mo", RruleHumanizer.wday_abbr(monday, :de) + assert_equal "Di", RruleHumanizer.wday_abbr(monday + 1.day, :de) + assert_equal "Mi", RruleHumanizer.wday_abbr(monday + 2.days, :de) + assert_equal "Do", RruleHumanizer.wday_abbr(monday + 3.days, :de) + assert_equal "Fr", RruleHumanizer.wday_abbr(monday + 4.days, :de) + assert_equal "Sa", RruleHumanizer.wday_abbr(monday + 5.days, :de) + assert_equal "So", RruleHumanizer.wday_abbr(monday + 6.days, :de) + end + + test "wday_abbr falls back to :de for an unrecognized locale" do + assert_equal "Mo", RruleHumanizer.wday_abbr(Time.parse("2026-07-06"), :fr) + end end diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb index 2da82d7e..a7ed478d 100644 --- a/test/models/helpers/content_helper_test.rb +++ b/test/models/helpers/content_helper_test.rb @@ -1,4 +1,8 @@ require 'test_helper' class ContentHelperTest < ActionView::TestCase + test "weekday_abbr delegates through the current I18n locale" do + I18n.locale = :de + assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06")) + end end diff --git a/test/models/helpers/nodes_helper_test.rb b/test/models/helpers/nodes_helper_test.rb index 13011dea..5d91a885 100644 --- a/test/models/helpers/nodes_helper_test.rb +++ b/test/models/helpers/nodes_helper_test.rb @@ -1,4 +1,25 @@ require 'test_helper' class NodesHelperTest < ActionView::TestCase + FakePage = Struct.new(:tag_list) + + test "default_event_tag_mapping matches erfa-detail" do + page = FakePage.new(["erfa-detail"]) + assert_equal ["erfa-detail", "open-day"], default_event_tag_mapping(page) + end + + test "default_event_tag_mapping matches chaostreff-detail" do + page = FakePage.new(["chaostreff-detail"]) + assert_equal ["chaostreff-detail", "open-day"], default_event_tag_mapping(page) + end + + test "default_event_tag_mapping returns nil for unrelated tags" do + page = FakePage.new(["update"]) + assert_nil default_event_tag_mapping(page) + end + + test "default_event_tag_list is nil without a matching tag" do + page = FakePage.new([]) + assert_nil default_event_tag_list(page) + end end -- cgit v1.3 From 9e63a6bec1b4ccc45dd684f7b6a941b75f9b9cf0 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 6 Jul 2026 06:09:21 +0200 Subject: Add public preview links for drafts Lets editors share a draft with people outside the admin system, via a random per-page token rather than an enumerable id - /preview/. shared_previews#show is intentionally unauthenticated. Redirects to the real public URL once the page is published. Surfaced on nodes#show (Admin Preview + Public Preview, next to Public Link) as generate/revoke buttons. --- app/controllers/nodes_controller.rb | 18 ++++++++++++++++++ app/controllers/shared_previews_controller.rb | 13 +++++++++++++ app/views/nodes/show.html.erb | 17 +++++++++++++++++ config/routes.rb | 4 ++++ 4 files changed, 52 insertions(+) create mode 100644 app/controllers/shared_previews_controller.rb (limited to 'app/views/nodes/show.html.erb') diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 380a6597..69aa2683 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -104,6 +104,24 @@ class NodesController < ApplicationController redirect_to node_path(@node) end + def generate_shared_preview + @node = Node.find(params[:id]) + if @node.draft + @node.draft.ensure_preview_token! + flash[:notice] = "Shareable preview link created - see below." + else + flash[:notice] = "Create or edit a draft first - shared preview links are only available for pages with an active draft." + end + redirect_to node_path(@node) + end + + def revoke_shared_preview + @node = Node.find(params[:id]) + @node.draft.revoke_preview_token! if @node.draft + flash[:notice] = "Shareable preview link revoked." + redirect_to node_path(@node) + end + def parameterize_preview render plain: slug_for(params[:title]) end diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb new file mode 100644 index 00000000..a8a540f1 --- /dev/null +++ b/app/controllers/shared_previews_controller.rb @@ -0,0 +1,13 @@ +class SharedPreviewsController < ApplicationController + def show + @page = Page.find_by!(preview_token: params[:token]) + + if @page.node && @page.node.head_id == @page.id + redirect_to node_path(@page.node) + return + end + + response.headers['X-Robots-Tag'] = 'noindex' + render template: @page.valid_template, layout: "application" + end +end diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index c533a55e..d63df86d 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -12,6 +12,23 @@ Public Link <%= link_to @page.public_link, content_url(@node.unique_path) %> + <% if @node.draft %> + + Admin Preview + <%= link_to preview_page_path(@node.draft), preview_page_path(@node.draft) %> + + + Public Preview + + <% if @node.draft.preview_token.present? %> + <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> + <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %> + <% else %> + <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %> + <% end %> + + + <% end %> Author <%= @page.user.try(:login) %> diff --git a/config/routes.rb b/config/routes.rb index 6c074143..26c3d4d2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,8 @@ Cccms::Application.routes.draw do member do put :unlock put :publish + put :generate_shared_preview + put :revoke_shared_preview end resources :revisions do @@ -50,6 +52,8 @@ Cccms::Application.routes.draw do end end + get 'preview/:token', to: 'shared_previews#show', as: :shared_preview + scope '/admin' do resources :assets end -- cgit v1.3 From 36b5324900968afc7498950c14ad13be5c061a7c Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 6 Jul 2026 16:49:45 +0200 Subject: Restructure nodes#show and nodes#new around node_description/node_content nodes#show's flat of label/value pairs grew unreadable once Admin Preview and Public Preview were added alongside Public Link - switched to the div.node_description/div.node_content pattern already used by nodes#edit, grouped into People/Dates/Links/Revisions/Tags/ Events/Children sections rather than one undifferentiated list. People/Dates/Links use a flex-based item layout, not a second
- multiple independent tables sharing one outer width but different column counts produced misaligned columns with no shared grid. Flex items size to their own content with a shared minimum instead. Revisions and Children are both collapsed via
, no JS needed - previously nodes#show didn't list a node's children at all, blocking the ability to find/navigate erfa and chaostreff pages without already knowing they're now standalone nodes. Revision list items link to node_revision_path(@node, page) - the actual per-revision diff/restore view, not the plain index. nodes#new converted to the same pattern for visual consistency, plus a few things surfaced along the way: submit buttons were unstyled site-wide (a bare input[type=submit] with no border, fill, or hover state - Create on this exact page was easy to miss entirely), fixed with a bordered/bold treatment reusing the existing form.button_to visual language rather than introducing a fourth button style. Title regained the bold weight it had before the table rewrite dropped it silently. Tag list grouping and full link/button semantic taxonomy (show vs edit/add vs publish/revoke vs destroy, applied consistently across every link on the page, not just buttons) are known follow-ups, not attempted here. --- app/views/nodes/new.html.erb | 57 ++++++------ app/views/nodes/show.html.erb | 204 ++++++++++++++++++++++++------------------ public/stylesheets/admin.css | 64 +++++++++---- 3 files changed, 192 insertions(+), 133 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index f5574547..0a053250 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb @@ -6,13 +6,11 @@ <% end %> -

What kind of node do you want to create?

- <%= form_tag nodes_path do %> -
- - - - - - - - - - - - - - - - - - - - -
Type +
+ +
Type
+
<% CccConventions::NODE_KINDS.each do |kind, config| %>

<%= radio_button_tag :kind, kind, kind == "generic", @@ -23,35 +21,32 @@ <% end %>

<% end %> -
Title<%= text_field_tag :title %> + + +
Title
+
+ <%= text_field_tag :title %> A URL slug will be generated from this automatically. You can review or adjust it afterward under the "metadata" section's Slug field. -
Parent + + +
+
Parent
+
<%= text_field_tag :parent_search_term, @parent_name %> <%= hidden_field_tag :parent_id, @parent_id %> -
- -
-
Resulting path +
+ + + +
Resulting path
+
This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it). -
<%= submit_tag "Create" %>
+ + +
+
<%= submit_tag "Create" %>
+ + <% end %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index d63df86d..189adb89 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -1,95 +1,129 @@ <% content_for :subnavigation do %> <%= link_to 'Edit', edit_node_path(@node), :class => "unselected" %> <%= link_to 'Preview', preview_page_path(@page) %> - <%= link_to 'Revisions', node_revisions_path(@node) %> - <%= unlock_link if @node.locked? %> <% end %> -
- - - - - - <% if @node.draft %> - - - - - - - - - <% end %> - - - - - - - - - <% if @page.node.locked? %> - - - - - <% end %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public Link<%= link_to @page.public_link, content_url(@node.unique_path) %>
Admin Preview<%= link_to preview_page_path(@node.draft), preview_page_path(@node.draft) %>
Public Preview - <% if @node.draft.preview_token.present? %> - <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> - <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %> - <% else %> - <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %> +
+ +
People
+
+
+
+ Author + <%= @page.user.try(:login) %> +
+
+ Editor + <%= @page.editor.try(:login) %> +
+ <% if @node.locked? %> +
+ Locked by + <%= @node.lock_owner.login %> + <%= unlock_link %> +
<% end %> -
Author<%= @page.user.try(:login) %>
Editor<%= @page.editor.try(:login) %>
Locked by<%= @page.node.lock_owner.login %>
Last updated<%= @page.updated_at %>
Published at<%= @page.published_at %>
Revision<%= @page.revision %>
Tagged with:<%= @page.tag_list %>
Events -
    - <% @node.events.order(:start_time).each do |event| %> -
  • - <%= event_schedule_text(event) %> - [<%= link_to 'show', event_path(event, return_to: request.path) %> - | <%= link_to 'edit', edit_event_path(event, return_to: request.path) %>] -
  • + + + +
    Dates
    +
    +
    +
    + Last updated + <%= @page.updated_at %> +
    + <% if @page.published_at.present? %> +
    + <%= @page.public? ? 'Published at' : 'Will publish at' %> + <%= @page.published_at %> +
    + <% end %> +
    +
    + +
    Links
    +
    +
    +
    + Public + <%= link_to @page.public_link, content_url(@node.unique_path) %> +
    + <% if @node.draft %> +
    + Admin Preview + <%= link_to preview_page_path(@node.draft), preview_page_path(@node.draft) %> +
    +
    + Public Preview + <% if @node.draft.preview_token.present? %> + <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> + <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %> + <% else %> + <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %> + <% end %> +
    <% end %> +
    +
    + +
    Revisions
    +
    +
    + + <%= pluralize(@node.pages.count, 'revision', 'revisions') %> + · <%= link_to 'full history (diff / restore)', node_revisions_path(@node) %> + +
      + <% @node.pages.order(:revision).each do |page| %> +
    • <%= link_to "##{page.revision} — #{page.title} (#{page.user.try(:login)}, #{page.updated_at})", node_revision_path(@node, page) %>
    • + <% end %>
    - <% mapping = default_event_tag_mapping(@page) %> - <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %> -
Title<%= sanitize( @page.title ) %>
Abstract<%= sanitize( @page.abstract ) %>
Body<%= sanitize( @page.body ) %>
+ +
+ +
Tags
+
<%= @page.tag_list %>
+ +
Events
+
+
    + <% @node.events.order(:start_time).each do |event| %> +
  • + <%= event_schedule_text(event) %> + [<%= link_to 'show', event_path(event, return_to: request.path) %> + | <%= link_to 'edit', edit_event_path(event, return_to: request.path) %>] +
  • + <% end %> +
+ <% mapping = default_event_tag_mapping(@page) %> + <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %> +
+ + <% if @node.children.any? %> +
Children
+
+
+ <%= pluralize(@node.children.count, 'child', 'children') %> +
    + <% @node.children.order(:slug).each do |child| %> +
  • <%= link_to (child.head&.title || child.draft&.title || child.slug), node_path(child) %>
  • + <% end %> +
+
+
+ <% end %> + +
Title
+
<%= sanitize(@page.title) %>
+ +
Abstract
+
<%= sanitize(@page.abstract) %>
+ +
Body
+
<%= sanitize(@page.body) %>
+ <%# Assets not yet addressed - no confirmed model/association seen for this page's attached assets %> + + diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index f240bb23..1196d830 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -106,7 +106,6 @@ div.pagination span.current, div.pagination a:hover { #main_navigation a, #sub_navigation a, #metadata a, -input[type=submit], #overview_toggle a { letter-spacing: 1px; padding-left: 5px; @@ -316,17 +315,23 @@ input[type=password] { border: 1px solid #989898; } -input[type=submit] { - border: none; - padding-top: 0px; - padding-bottom: 1px; - background-color: #ffffff; +input[type="submit"] { + -webkit-appearance: none; + appearance: none; + background: none; + border: 1px solid #000000; + border-radius: 2px; + padding: 4px 12px; + font: inherit; + font-weight: bold; + color: inherit; + cursor: pointer; + text-decoration: none; } -input[type=submit]:hover { - border: none; +input[type="submit"]:hover { color: #ffffff; - background-color: #ff9600; + background-color: #000000; } input[type=radio] { @@ -516,6 +521,39 @@ table tr.header { padding-bottom: 4px; } +.node_content.node_info_group { + border: 1px solid #e8e8e8; + border-radius: 6px; + padding: 0.5rem 0.75rem; +} + +.node_info_group_items { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 2rem; +} + +.node_info_item { + min-width: 140px; +} + +.node_info_label { + display: block; + font-weight: normal; + color: #969696; + font-size: 0.85rem; + text-transform: lowercase; +} + +.node_info_group ul { + margin: 0; + padding-left: 1.25rem; +} + +.node_info_group li { + margin-bottom: 0.25rem; +} + #search_widget { position: absolute; top: 20px; @@ -569,14 +607,6 @@ table tr.header { border-bottom: none; } -table#content th.description { - width: 100px; -} - -table#content th.content { - width: 690px; -} - #menu_item_list { border-collapse: collapse; padding: 5px 5px 5px 5px; -- cgit v1.3 From 1bf719d6ac58187cf406d92a40b665d3fa6e658b Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 7 Jul 2026 03:48:09 +0200 Subject: Add context-aware child-creation shortcuts to nodes#show parent_match Procs on CccConventions::NODE_KINDS, matched against unique_path, decide which "add child" kinds show on a given node. Fixes nodes#new not honoring a pre-selected kind (radio group and parent-field visibility both defaulted to "generic" unconditionally). --- app/controllers/nodes_controller.rb | 1 + app/helpers/nodes_helper.rb | 5 +++ app/views/nodes/new.html.erb | 4 +-- app/views/nodes/show.html.erb | 29 +++++++++++++----- lib/ccc_conventions.rb | 61 ++++++++++++++++++++----------------- public/javascripts/admin_search.js | 18 +++++++---- public/stylesheets/admin.css | 8 +++++ 7 files changed, 82 insertions(+), 44 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 69aa2683..a72be68e 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -22,6 +22,7 @@ class NodesController < ApplicationController def new @node = Node.new node_params + @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" if params.has_key?(:parent_id) @parent_id = params[:parent_id] @parent_name = Node.find(@parent_id).title diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb index 2baf813c..a89f8799 100644 --- a/app/helpers/nodes_helper.rb +++ b/app/helpers/nodes_helper.rb @@ -58,4 +58,9 @@ module NodesHelper t(:event_schedule_none) end end + + def matching_node_kinds(node) + path = node.unique_path + CccConventions::NODE_KINDS.select { |_, config| config[:parent_match]&.call(path) } + end end diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index 0a053250..71f2fbf1 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb @@ -13,7 +13,7 @@
<% CccConventions::NODE_KINDS.each do |kind, config| %>

- <%= radio_button_tag :kind, kind, kind == "generic", + <%= radio_button_tag :kind, kind, kind == @selected_kind, data: { path_prefix: resolve_kind_text(config[:path_prefix]) } %> <%= resolve_kind_text(config[:label]) %> <% if config[:hint] %> @@ -29,7 +29,7 @@ A URL slug will be generated from this automatically. You can review or adjust it afterward under the "metadata" section's Slug field.

-
+
">
Parent
<%= text_field_tag :parent_search_term, @parent_name %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 189adb89..963bc37c 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -101,17 +101,30 @@ <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %>
- <% if @node.children.any? %> + <% matches = matching_node_kinds(@node) %> + <% if @node.children.any? || matches.any? %>
Children
-
- <%= pluralize(@node.children.count, 'child', 'children') %> -
    - <% @node.children.order(:slug).each do |child| %> -
  • <%= link_to (child.head&.title || child.draft&.title || child.slug), node_path(child) %>
  • + <% if @node.children.any? %> +
    + <%= pluralize(@node.children.count, 'child', 'children') %> +
      + <% @node.children.order(:slug).each do |child| %> +
    • <%= link_to (child.head&.title || child.draft&.title || child.slug), node_path(child) %>
    • + <% end %> +
    +
    + <% end %> + <% if matches.any? %> +
-
+

+ <% end %>
<% end %> diff --git a/lib/ccc_conventions.rb b/lib/ccc_conventions.rb index c0f9943f..b4204528 100644 --- a/lib/ccc_conventions.rb +++ b/lib/ccc_conventions.rb @@ -4,44 +4,49 @@ module CccConventions NODE_KINDS = { "top_level" => { - parent: -> { Node.root }, - path_prefix: "", - label: "Top Level" + parent: -> { Node.root }, + parent_match: ->(path) { path == [] }, + path_prefix: "", + label: "Top Level" }, "generic" => { - label: "Generic", - hint: "Can be created anywhere - choose the parent below." - # no path_prefix - depends on whatever parent gets chosen; see below + parent_match: ->(path) { true }, + label: "Generic", + hint: "Can be created anywhere - choose the parent below." }, "update" => { - parent: -> { Update.find_or_create_parent }, - tags: ["update"], - path_prefix: -> { "updates/#{Time.now.year}" }, - label: "Update", - hint: -> { "Automatically created in /updates/#{Time.now.year}/, gets tag \"update\", and inherits the update template." } + parent: -> { Update.find_or_create_parent }, + parent_match: ->(path) { path[0] == "updates" && (path.length == 1 || path[1] =~ /\A\d{4}\z/) }, + tags: ["update"], + path_prefix: -> { "updates/#{Time.now.year}" }, + label: "Update", + hint: -> { "Automatically created in /updates/#{Time.now.year}/, gets tag \"update\", and inherits the update template." } }, "press_release" => { - parent: -> { Update.find_or_create_parent }, - tags: ["update", "pressemitteilung"], - path_prefix: -> { "updates/#{Time.now.year}" }, - label: "Pressemitteilung", - hint: -> { "Automatically created in /updates/#{Time.now.year}/, gets tags \"update, pressemitteilung\", and inherits the update template." } + parent: -> { Update.find_or_create_parent }, + parent_match: ->(path) { path[0] == "updates" && (path.length == 1 || path[1] =~ /\A\d{4}\z/) }, + tags: ["update", "pressemitteilung"], + path_prefix: -> { "updates/#{Time.now.year}" }, + label: "Pressemitteilung", + hint: -> { "Automatically created in /updates/#{Time.now.year}/, gets tags \"update, pressemitteilung\", and inherits the update template." } }, "erfa" => { - parent: -> { Node.find_by_unique_name!(ERFA_PARENT_NAME) }, - tags: ["erfa-detail"], - template: "chapter_detail", - path_prefix: ERFA_PARENT_NAME, - label: "Erfa", - hint: "Automatically created under the Erfa-Kreise overview page, gets tag \"erfa-detail\", and uses the chapter detail template." + parent: -> { Node.find_by_unique_name!(ERFA_PARENT_NAME) }, + parent_match: ->(path) { path == ["club", "erfas"] }, + tags: ["erfa-detail"], + template: "chapter_detail", + path_prefix: ERFA_PARENT_NAME, + label: "Erfa", + hint: "Automatically created under the Erfa-Kreise overview page, gets tag \"erfa-detail\", and uses the chapter detail template." }, "chaostreff" => { - parent: -> { Node.find_by_unique_name!(CHAOSTREFF_PARENT_NAME) }, - tags: ["chaostreff-detail"], - template: "chapter_detail", - path_prefix: CHAOSTREFF_PARENT_NAME, - label: "Chaostreff", - hint: "Automatically created under the Chaostreffs overview page, gets tag \"chaostreff-detail\", and uses the chapter detail template." + parent: -> { Node.find_by_unique_name!(CHAOSTREFF_PARENT_NAME) }, + parent_match: ->(path) { path == ["club", "chaostreffs"] }, + tags: ["chaostreff-detail"], + template: "chapter_detail", + path_prefix: CHAOSTREFF_PARENT_NAME, + label: "Chaostreff", + hint: "Automatically created under the Chaostreffs overview page, gets tag \"chaostreff-detail\", and uses the chapter detail template." } }.freeze end diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 25659298..6ef9087a 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -208,14 +208,20 @@ parent_search = { }, initialize_radio_buttons : function() { - $("input[name='kind']").bind("change", function(){ - if ($(this).val() === "generic") { - $("#parent_search_field").show(); - } else { - $("#parent_search_field").hide(); - } + parent_search.sync_parent_field(); + $("input[name='kind']").bind("change", function() { + parent_search.sync_parent_field(); parent_search.update_resulting_path(); }); + }, + + sync_parent_field : function() { + var kind = $("input[name='kind']:checked").val(); + if (kind === "generic") { + $("#parent_search_field").show(); + } else { + $("#parent_search_field").hide(); + } } } diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 1196d830..3033798c 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -338,6 +338,14 @@ input[type=radio] { border: 1px solid #989898; } +.add_child_links { + margin-top: 0.5rem; +} + +.add_child_links a { + white-space: nowrap; +} + div#login_form input[type=text], div#login_form input[type=password] { width: 150px; } -- cgit v1.3 From 42f81016be8c55322e0dbb2c20b08812d5d2228b Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 7 Jul 2026 21:43:21 +0200 Subject: Adopt button pills in views --- app/helpers/link_helper.rb | 2 +- app/views/admin/_drafts.html.erb | 2 +- app/views/nodes/edit.html.erb | 2 +- app/views/nodes/show.html.erb | 4 ++-- app/views/revisions/index.html.erb | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 878e8e4a..9b0b4e10 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -44,7 +44,7 @@ module LinkHelper "Last modified #{@page.updated_at.to_fs(:db)}" button_to 'Unlock', unlock_node_path(@node), method: :put, - form: { data: { confirm: message } } + form: { data: { confirm: message }, class: 'button_to state_changing' } end def content_path(page_path = nil, options = {}) diff --git a/app/views/admin/_drafts.html.erb b/app/views/admin/_drafts.html.erb index a35b0ab8..4ef568f8 100644 --- a/app/views/admin/_drafts.html.erb +++ b/app/views/admin/_drafts.html.erb @@ -17,7 +17,7 @@ <%= link_to 'Show', node_path(draft.node) %> <%= link_to "Revisions", revision_path(draft.node.id) %> - <%= button_to "Publish", publish_node_path(draft.node), method: :put, form: { data: { confirm: "Do you really want to publish?" } } %> + <%= button_to "Publish", publish_node_path(draft.node), method: :put, form: { data: { confirm: "Do you really want to publish?" }, class: 'button_to state_changing' } %> <% end %> diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 2d0edc16..4cdb3ae0 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -2,7 +2,7 @@ <%= link_to 'metadata', '#', :id => 'button', :class => "unselected" %> <%= link_to 'Show', @node %> <%= link_to 'Preview', preview_page_path(@draft) %> - <%= button_to 'Publish', publish_node_path(@node), method: :put, form: { data: { confirm: "Publish this draft?" } } %> + <%= button_to 'Publish', publish_node_path(@node), method: :put, form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %> <%= link_to 'Revisions', node_revisions_path(@node) %> <% end %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 963bc37c..71dbaa3e 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -59,9 +59,9 @@ Public Preview <% if @node.draft.preview_token.present? %> <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> - <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %> + <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." }, class: 'button_to state_changing' } %> <% else %> - <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %> + <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put, form: { class: 'button_to state_changing' } %> <% end %>
<% end %> diff --git a/app/views/revisions/index.html.erb b/app/views/revisions/index.html.erb index b875a4fa..26a9ff45 100644 --- a/app/views/revisions/index.html.erb +++ b/app/views/revisions/index.html.erb @@ -27,7 +27,7 @@ <%= button_to 'restore', restore_node_revision_path(@node, page), method: :put, - form: { data: { confirm: "Restore this revision?" } } %> + form: { data: { confirm: "Restore this revision?" }, class: 'button_to state_changing' } %> <% end %> -- cgit v1.3 From b555a3e05c44f21067855b6bbafa70a5e32c9a40 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:56:39 +0200 Subject: Promote page title to headline in nodes#show --- app/views/nodes/show.html.erb | 5 +---- 1 file changed, 1 insertion(+), 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 71dbaa3e..2ab7986e 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -4,8 +4,8 @@ <% end %>
+

<%= title_for_node(@node) %>

-
People
@@ -128,9 +128,6 @@
<% end %> -
Title
-
<%= sanitize(@page.title) %>
-
Abstract
<%= sanitize(@page.abstract) %>
-- cgit v1.3 From 168ff3f8b6037bc3c2b5bce74adac37e48366dac Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 22:52:00 +0200 Subject: Add the Status section to nodes#show and rebuild nodes#edit's action bar Retires content_for :subnavigation on nodes#show entirely -- Preview was fully redundant with Links' own preview URLs in every state, and Edit is now a permanent, always-rendered action inside the new Status section rather than a link floating at the top, consistent with the "action lives next to the info" pattern People already established for Unlock. Status surfaces head/draft/autosave plainly and gates Edit/Publish/Destroy/Discard on lock ownership specifically, not mere lock presence -- @node.locked? alone would have blocked the lock owner's own session, caught by the click-test and fixed here rather than shipped. nodes#edit's action bar is rebuilt to sit outside form_for (both button_to calls render their own nested form, invalid HTML the browser was silently stripping) with Save wired back in via form="..." rather than needing to live inside the form tag at all. Also brings the locked-and-ready-to-edit flash message, and the visual polish from this session's click-test: consistent button heights across the bordered and pill-shaped variants sharing a row, spacing between Status's data and its actions, and error_messages styling reusing the destructive-red vocabulary already established elsewhere. --- app/controllers/nodes_controller.rb | 2 + app/views/nodes/edit.html.erb | 45 ++++++++++--------- app/views/nodes/show.html.erb | 58 +++++++++++++++++++++--- public/stylesheets/admin.css | 88 ++++++++++++++++++++++++++++++++++--- 4 files changed, 159 insertions(+), 34 deletions(-) (limited to 'app/views/nodes/show.html.erb') diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index d8586e29..72d4a3eb 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -68,6 +68,8 @@ class NodesController < ApplicationController @node.lock_for_editing!( current_user ) @page = @node.autosave || @node.draft || @node.head + flash.now[:notice] = "Node locked and ready to edit" unless @node.autosave + if @node.autosave flash.now[:notice] = "This page has unsaved changes from a previous session, shown below. " \ diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index feba92ab..cdc9b36e 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -1,3 +1,21 @@ +

<%= title_for_node(@node) %>

+ +
+ <%= button_to 'Unlock + Back', unlock_node_path(@node), method: :put, + form: { class: 'button_to state_changing' }, + disabled: @node.autosave.present? %> + + <% if @node.autosave || (@node.draft && @node.head) %> + <%= 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 %> + + <%= submit_tag "Save Draft", form: dom_id(@node, :edit) %> + <%= submit_tag "Save + Unlock + Exit", form: dom_id(@node, :edit) %> + <%= link_to "Preview ↗", preview_page_path(@page), target: "_blank", rel: "noopener", class: "preview_link" %> +
+
<%= form_for(@node, html: { data: { autosave_url: autosave_node_path(@node), show_url: node_path(@node) } }) do |f| %> <% if @node.errors.any? %> @@ -6,25 +24,6 @@
<% end %> -

<%= title_for_node(@node) %>

- -
- <%= button_to 'Unlock + Back', unlock_node_path(@node), method: :put, - form: { class: 'button_to state_changing' }, - disabled: @node.autosave.present? %> - - <% if @node.autosave || (@node.draft && @node.head) %> - <%= 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 %> - - <%= link_to 'Preview', preview_page_path(@page) %> - - <%= f.submit 'Save Draft' %> - <%= f.submit 'Save + Unlock + Exit' %> -
-
Metadata (slug, parent, tags, template, author, images)
@@ -102,9 +101,9 @@
<%= d.text_area :body, :class => 'with_editor' %>
<% end %> -
- <%= f.submit 'Save Draft' %> - <%= f.submit 'Save + Unlock + Exit' %> -
+
+ <%= f.submit 'Save Draft' %> + <%= f.submit 'Save + Unlock + Exit' %> +
<% end %>
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 2ab7986e..036caf29 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -1,11 +1,59 @@ -<% content_for :subnavigation do %> - <%= link_to 'Edit', edit_node_path(@node), :class => "unselected" %> - <%= link_to 'Preview', preview_page_path(@page) %> -<% end %> - +<% locked_by_other = @node.locked? && @node.lock_owner != current_user %>

<%= title_for_node(@node) %>

+
Status
+
+
+
+ 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 %> +
+
People
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index ceacf174..a6e8bf6e 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 3fa05194..6ab21358 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 38d42d90..d1538e11 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 9acb26f4..4b0c5491 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 fdb51f8b..a6290133 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 7675ab64..82d99540 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 77b45f4c..c67ccb35 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 %>)

+ @@ -92,6 +92,7 @@ + <% @drafts.each do |node| %> "> @@ -103,6 +104,9 @@ + <% end %>
IDActions Locked by Rev.Autosave
<%= 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" : "" %> +
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index cdc9b36e..1c194101 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 036caf29..24693108 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 d7bb528d..3157dcaa 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 c0aef2ff..da6b6266 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 9bbf29bc..d6005ba6 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 caca6bfc..162e6f1c 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 2138c19d..9e71dec5 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 d1538e11..6fcd930c 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 24693108..8b9e98b0 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 3157dcaa..490cf172 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 f3e04c27..ce3419cf 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 8b9e98b0..5756649e 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 %>
+
Tags
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 1bb6cf4c..e4bdac41 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 5756649e..07cb5d13 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 37091d5a..b563d4da 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 6591091e..8c355617 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 %> diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb index 02866780..5717dd99 100644 --- a/app/views/assets/show.html.erb +++ b/app/views/assets/show.html.erb @@ -6,7 +6,9 @@
- <%= link_to 'Edit', edit_asset_path(@asset), class: 'action_button' %> + <%= link_to edit_asset_path(@asset), class: 'action_button' do %> + <%= icon("edit", library: "tabler", "aria-hidden": true) %> Edit + <% end %>
<%= link_to 'Back', assets_path %> diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb index 45b084f0..b6564a4e 100644 --- a/app/views/events/edit.html.erb +++ b/app/views/events/edit.html.erb @@ -2,8 +2,10 @@
<%= link_to 'Back', safe_return_to(params[:return_to] || events_path) %> - <%= button_to 'Destroy', event_path(@event), method: :delete, - form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } %> + <%= button_to event_path(@event), method: :delete, + form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy + <% end %>
<%= form_for(@event) do |f| %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 3d953d5f..dde897ee 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -1,7 +1,11 @@

Events

-<%= link_to 'New event', new_event_path, class: 'action_button' %> -<%= link_to 'View events without a page →', without_node_events_path %> +
+ <%= link_to new_event_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create event + <% end %> + <%= link_to 'View events without a page →', without_node_events_path %> +
<%= will_paginate @events %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 58122280..ac084c92 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -11,10 +11,14 @@
<% end %>
- <%= link_to 'Edit', edit_event_path(@event, return_to: request.path), class: 'action_button' %> + <%= link_to edit_event_path(@event, return_to: request.path), class: 'action_button' do %> + <%= icon("edit", library: "tabler", "aria-hidden": true) %> Edit + <% end %>
- <%= button_to 'Destroy', event_path(@event), method: :delete, form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } %> + <%= button_to event_path(@event), method: :delete, form: { data: { confirm: 'Delete this event?' }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy + <% end %>
diff --git a/app/views/events/without_node.html.erb b/app/views/events/without_node.html.erb index 52514ab9..bb27173b 100644 --- a/app/views/events/without_node.html.erb +++ b/app/views/events/without_node.html.erb @@ -1,7 +1,9 @@

Events without a node

<%= link_to '← All events', events_path %> -<%= link_to 'New event', new_event_path, class: 'action_button' %> +<%= link_to new_event_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create event +<% end %> <%= will_paginate @events %> diff --git a/app/views/menu_items/index.html.erb b/app/views/menu_items/index.html.erb index e84199b3..591eb68d 100644 --- a/app/views/menu_items/index.html.erb +++ b/app/views/menu_items/index.html.erb @@ -1,6 +1,8 @@

Menu Items

-<%= link_to 'New menu item', new_menu_item_path, class: 'action_button' %> +<%= link_to new_menu_item_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create menu item +<% end %> <% @menu_items.each do |menu_item| %> @@ -11,9 +13,10 @@ <% end %> diff --git a/app/views/nodes/_node_list.html.erb b/app/views/nodes/_node_list.html.erb index 03f38b4c..f7210f12 100644 --- a/app/views/nodes/_node_list.html.erb +++ b/app/views/nodes/_node_list.html.erb @@ -4,7 +4,9 @@ <% end %> <%= hidden_field_tag :tags, params[:tags] if params[:tags].present? %> <%= text_field_tag :q, params[:q], placeholder: "Search title, abstract, body…" %> - <%= submit_tag "Search", class: "action_button" %> + <%= button_tag type: "submit", class: "action_button" do %> + <%= icon("search", library: "tabler", "aria-hidden": true) %> Search + <% end %> <% if params[:q].present? || params[:kinds].present? || params[:tags].present? %> <%= link_to "Reset", url_for(controller: params[:controller], action: params[:action]) %> <% end %> diff --git a/app/views/nodes/chapters.html.erb b/app/views/nodes/chapters.html.erb index 939f19cd..543bc53c 100644 --- a/app/views/nodes/chapters.html.erb +++ b/app/views/nodes/chapters.html.erb @@ -1,9 +1,11 @@

Chapters

-<%= form_tag chapters_nodes_path, method: :get do %> +<%= form_tag chapters_nodes_path, method: :get, class: "node_search_form" do %> - <%= submit_tag "Filter", class: "action_button" %> + <%= button_tag type: "submit", class: "action_button" do %> + <%= icon("filter", library: "tabler", "aria-hidden": true) %> Filter + <% end %> <% end %> <%= render 'node_list' %> diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 13b78fca..1ac05e08 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -6,9 +6,11 @@ disabled: @node.autosave.present? %> <% if @node.autosave || (@node.draft && @node.head) %> - <%= 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' } %> + <%= button_to revert_node_path(@node), method: :put, + form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> + <%= @node.draft && !@node.autosave ? "Destroy Draft" : "Discard Autosave" %> + <% end %> <% if pair = @node.available_layer_pairs.find { |p| p.include?(:autosave) } %> <%= button_to 'What changed?', diff_node_revisions_path(@node), diff --git a/app/views/nodes/index.html.erb b/app/views/nodes/index.html.erb index 2688ea12..3bb251c5 100644 --- a/app/views/nodes/index.html.erb +++ b/app/views/nodes/index.html.erb @@ -1,7 +1,9 @@

Nodes

- <%= link_to 'New node', new_node_path, class: 'action_button' %> + <%= link_to new_node_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create node + <% end %> Creates a node with no parent or kind set — for a specific page, use the wizard's create flow or "add child" from an existing node instead.
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 07cb5d13..6e202582 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -25,7 +25,10 @@ <% 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" %> + <%= link_to edit_node_path(@node), class: "action_button" do %> + <%= icon("edit", library: "tabler", "aria-hidden": true) %> + <%= @node.autosave ? "Continue Editing" : (@node.draft ? "Edit Draft" : "Lock + Edit") %> + <% end %> <% if !@node.draft && !@node.autosave %> Nothing pending — this will start a fresh draft. <% end %> @@ -51,10 +54,12 @@ <% end %> <% if @node.autosave || (@node.draft && @node.head) %>
- <%= 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' } %> + <%= button_to revert_node_path(@node), method: :put, + params: { return_to: request.path }, + form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> + <%= @node.draft && !@node.autosave ? "Destroy Draft" : "Discard Autosave" %> + <% end %>
<% end %> <% end %> diff --git a/app/views/occurrences/index.html.erb b/app/views/occurrences/index.html.erb index 82818d09..aa084ed9 100644 --- a/app/views/occurrences/index.html.erb +++ b/app/views/occurrences/index.html.erb @@ -18,7 +18,9 @@ - + <% end %> diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb index 05c73117..da67871c 100644 --- a/app/views/pages/index.html.erb +++ b/app/views/pages/index.html.erb @@ -12,7 +12,9 @@ <%=h page.title %> <%= link_to 'Show', content_path(:page_path => page.node.unique_path) %> <%= link_to 'Edit', edit_page_path(page) %> - <%= button_to 'Destroy', page, method: :delete, form: { data: { confirm: 'Are you sure?' } } %> + <%= button_to page, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy + <% end %> <% end %> diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index b9ce6bdd..5638b7ab 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -49,10 +49,12 @@ <% 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, - params: { return_to: request.fullpath }, - form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %> + <%= button_to revert_node_path(@node), method: :put, + params: { return_to: request.fullpath }, + form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> + <%= @node.draft && !@node.autosave ? "Destroy Draft" : "Discard Autosave" %> + <% end %> <% end %>

<% end %> diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb index 798b82b3..3a88f5e2 100644 --- a/app/views/users/_user.html.erb +++ b/app/views/users/_user.html.erb @@ -7,9 +7,10 @@ <%= link_to "edit", edit_user_path(user) %> - <%= button_to "destroy", user_path(user), - method: :delete, - form: { data: { confirm: "Do you really want to delete user #{user.login}?" }, class: 'button_to destructive' } %> + <%= button_to user_path(user), method: :delete, + form: { data: { confirm: "Do you really want to destroy user #{user.login}?" }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy + <% end %> <% end %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 46e958bf..4f5266d8 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -1,5 +1,7 @@

Admins

-<%= link_to 'New admin user', new_user_path(admin: true), class: 'action_button' %> +<%= link_to new_user_path(admin: true), class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create admin user +<% end %> @@ -11,7 +13,9 @@
Login

Users

-<%= link_to 'New user', new_user_path, class: 'action_button' %> +<%= link_to new_user_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create user +<% end %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index e744efeb..70881dbd 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -6,7 +6,9 @@
- <%= link_to 'Edit', edit_user_path(@user), class: 'action_button' %> + <%= link_to edit_user_path(@user), class: 'action_button' do %> + <%= icon("edit", library: "tabler", "aria-hidden": true) %> Edit + <% end %>
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 5cd5ad4d..58f8a86b 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -13,7 +13,7 @@ class UsersControllerTest < ActionController::TestCase login_as :aaron get :index assert_response :success - assert_select "button[type=submit]", "destroy" + assert_select "button[type=submit]", "Destroy" assert_select "a", "show" end -- cgit v1.3 From 65c7c40f74c315c1a39fb15da8ce341fb8b9b05e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 01:57:43 +0200 Subject: Wavy-underline: allowlist to default; destroy-button and search polish Flips the plain-link underline from an opt-in list (growing every time a new context needed it) to the default a rule, with opt-outs living as more specific overrides where something shouldn't look like plain content -- .action_button, the icon nav, the search dropdown's bold results. The allowlist's own history made the case for this: three of its entries were already hand-copied duplicates of the same five properties, and two of those had already silently drifted. form.button_to's icon+text buttons (Destroy, currently) get the same inline-flex/gap treatment .action_button already had -- they were never included in that fix, so they still baseline-aligned an SVG against text instead of centering it, plus an explicit icon size they'd never had either. Dashboard search results: tags render as a wrapped row of pills instead of one-per-line, for visual distinction from page results. The public-preview link and its Revoke button get their own row rather than loose inline flow, which wrapped unpredictably next to a long token URL -- aligned by box-top rather than center or baseline, since a
Login