From 9c630d186003a47563be2e3547485060dac7dd98 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 4 Jul 2026 01:34:42 +0200 Subject: Retire event_information, superseded by nodes#show's event manager The metadata-panel "Event" field on nodes#edit called event_information, a second, tag-less implementation of "list events + add one" - now redundant now that nodes#show has the tag-aware version with the auto-tag flash explanation. Confirmed via grep this was the only call site before removing both it and the method definition. --- app/views/nodes/edit.html.erb | 3 --- 1 file changed, 3 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 498fa1f..31f585c 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -15,9 +15,6 @@ <% end %>
-
Event
-
<%= event_information %>
-
Slug
<%= f.text_field( -- cgit v1.3 From 9a7ce465b06462443ee82e8418f3c070de080ab2 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 5 Jul 2026 21:52:11 +0200 Subject: Surface node-kind conventions in the admin UI nodes#new: each kind's radio button now shows a label plus a smaller, muted .field_hint explaining what happens automatically (parent, tags, template) - previously a single undifferentiated string mixing a short label with a long parenthetical, cluttering the smaller kinds (erfa/chaostreff) worst. Title field gets the same hint treatment, telling editors up front that the slug is auto-generated and where to adjust it later - the previous silence here meant editors only discovered slug generation after already committing to a title, with no visible way to fix it. New "Resulting path" row live-previews the full URL (origin + kind's path prefix + a debounced call to the new parameterize_preview route, reusing the exact same slug_for helper "create" now calls, so the preview can never diverge from what actually gets created) as the editor types, plus a copy-to-clipboard button. For "generic", the prefix comes from whichever parent gets chosen via the search widget, not the registry - parent_search.link_closure now stashes the chosen node's unique_name as a data attribute for exactly this. New read-only admin/conventions view dumps the NODE_KINDS registry as a reference table for whoever's confused later about how a given kind behaves - deliberately just renders the same label/hint text used on nodes#new rather than re-describing parent/tags/template separately, since decomposing the parent Proc would mean either invoking it (Update's has a real side effect - creates the year folder) or re-stating its logic a second time in a different shape, either of which risks drifting from the actual behavior. nodes#edit's Template field gets a one-line hint noting the value may already be pre-filled based on how the node was created - the one place the previous commit's new inheritance mechanism actually surfaces on this view. --- app/helpers/application_helper.rb | 4 ++++ app/views/nodes/edit.html.erb | 5 ++++- app/views/nodes/new.html.erb | 34 +++++++++++++++++++--------------- config/routes.rb | 1 + public/stylesheets/admin.css | 12 ++++++++++++ 5 files changed, 40 insertions(+), 16 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0be66e9..87aa82c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,4 +11,8 @@ module ApplicationHelper end end end + + def resolve_kind_text(value) + value.respond_to?(:call) ? value.call : value + end end diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 31f585c..2d0edc1 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -44,7 +44,10 @@
<%= d.datetime_select :published_at, :value => @draft.published_at %>
Template
-
<%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %>
+
+ <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> + Set automatically based on how this node was created - change it if needed. +
Author
<%= d.select :user_id, user_list %>
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index 028d727..f557454 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb @@ -13,27 +13,23 @@ Type -
- <%= radio_button_tag :kind, "top_level" %> - Top Level -
-

- <%= radio_button_tag :kind, "generic", :selected => true %> - Generic ( can be created anywhere ) -

-

- <%= radio_button_tag :kind, "update" %> - Update ( is automatically created in /updates/<%= Time.now.year.to_s %>/ and gets tag "update" ) -

+ <% CccConventions::NODE_KINDS.each do |kind, config| %>

- <%= radio_button_tag :kind, "press_release" %> - Pressemitteilung ( is automatically created in /updates/<%= Time.now.year.to_s %>/ and gets tags "update, pressemitteilung" ) + <%= radio_button_tag :kind, kind, kind == "generic", + data: { path_prefix: resolve_kind_text(config[:path_prefix]) } %> + <%= resolve_kind_text(config[:label]) %> + <% if config[:hint] %> + <%= resolve_kind_text(config[:hint]) %> + <% end %>

+ <% end %> Title - <%= text_field_tag :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 @@ -45,6 +41,14 @@
+ + 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" %> diff --git a/config/routes.rb b/config/routes.rb index cf4733c..6c07414 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,6 +59,7 @@ Cccms::Application.routes.draw do match 'admin' => 'admin#index', :as => :admin, :via => :get match 'admin/search' => 'admin#search', :as => :admin_search, :via => :get match 'admin/menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get + match 'admin/conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get match 'search' => 'search#index', :as => :search, :via => :get resources :users diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index a02bd61..f240bb2 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -508,6 +508,14 @@ table tr.header { text-align: left; } +.field_hint { + display: block; + margin-top: 2px; + font-size: 0.75rem; + color: #969696; + padding-bottom: 4px; +} + #search_widget { position: absolute; top: 20px; @@ -527,18 +535,21 @@ table tr.header { font-size: 18px; } +#menu_search_results p, #search_results p { margin: 0; padding: 4px 4px 0 4px; border-bottom: 1px solid #e8e8e8; } +#menu_search_results p a, #search_results p a { display: block; font-weight: bold; font-size: 0.95rem; } +#menu_search_results p span.result_path, #search_results p span.result_path { display: block; margin-top: 2px; @@ -548,6 +559,7 @@ table tr.header { padding-bottom: 4px; } +#menu_search_results p.search_more, #search_results p.search_more { margin: 0; padding: 6px 4px; -- 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/edit.html.erb') diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 878e8e4..9b0b4e1 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 a35b0ab..4ef568f 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 2d0edc1..4cdb3ae 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 963bc37..71dbaa3 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 b875a4f..26a9ff4 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 b034da9fbabb882fbe9f5f3f355442dfa20e5ecf Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:56:11 +0200 Subject: Tuck away metadata section in nodes#edit behind a summary/detail cascade --- app/views/nodes/edit.html.erb | 105 ++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 51 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 4cdb3ae..4693569 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -1,9 +1,7 @@ <% content_for :subnavigation do %> - <%= 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?" }, class: 'button_to state_changing' } %> - <%= link_to 'Revisions', node_revisions_path(@node) %> <% end %>
@@ -13,67 +11,72 @@
    <% @node.errors.full_messages.each do |msg| %>
  • <%= msg %>
  • <% end %>
<% end %> - -
-
Slug
-
- <%= f.text_field( - :staged_slug, :value => @node.staged_slug || @node.slug - ) - %> -
-
parent
-
- <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> -
+

<%= title_for_node(@node) %>

-
- <%= f.hidden_field( - :staged_parent_id, - :value => @node.staged_parent_id || @node.parent_id +
+ Metadata (slug, parent, tags, template, author, images) +
+
Slug
+
+ <%= f.text_field( + :staged_slug, :value => @node.staged_slug || @node.slug ) - %> -
- - <%= fields_for @draft do |d| %> -
Tags - comma seperated
-
<%= text_field_tag :tag_list, @draft.tag_list.join(', ') %>
+ %> +
-
Publish at
-
<%= d.datetime_select :published_at, :value => @draft.published_at %>
+
parent
+
+ <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> +
-
Template
-
- <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> - Set automatically based on how this node was created - change it if needed. -
+
+ <%= f.hidden_field( + :staged_parent_id, + :value => @node.staged_parent_id || @node.parent_id + ) + %> +
-
Author
-
<%= d.select :user_id, user_list %>
+ <%= fields_for @draft do |d| %> +
Tags - comma seperated
+
<%= text_field_tag :tag_list, @draft.tag_list.join(', ') %>
-
Images
-
-
    - <% @draft.assets.images.each do |image| %> -
  • - <%= image_tag(image.upload.url(:thumb)) %> -
  • - <% end %> -
-
- image browser +
Publish at
+
<%= d.datetime_select :published_at, :value => @draft.published_at %>
+ +
Template
+
+ <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> + Set automatically based on how this node was created - change it if needed.
-
-
    - <% Asset.images.each do |image| %> -
  • <%= image_tag(image.upload.url(:thumb)) %>
  • + +
    Author
    +
    <%= d.select :user_id, user_list %>
    + +
    Images
    +
    +
      + <% @draft.assets.images.each do |image| %> +
    • + <%= image_tag(image.upload.url(:thumb)) %> +
    • <% end %>
    + +
    +
      + <% Asset.images.each do |image| %> +
    • <%= image_tag(image.upload.url(:thumb)) %>
    • + <% end %> +
    +
-
- +
+
Title
<%= d.text_field :title %>
-- cgit v1.3 From 43e7743ebff1b1f41bbb5ae39bcc5bed7d8982e3 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 16:24:05 +0200 Subject: Autosave posts to its own endpoint surface lock loss immediately Previously piggybacked on the main form's action and relied on a .js.erb response to update #flash; now posts to the dedicated autosave route with an explicit _method=put override, and reacts to a 423 by stopping the timer and showing a persistent banner rather than letting the next Save discover the conflict on its own. --- app/views/nodes/edit.html.erb | 2 +- public/javascripts/admin_interface.js | 38 ++++++++++++++++++++++++++++++++--- public/stylesheets/admin.css | 5 +++++ 3 files changed, 41 insertions(+), 4 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 4693569..1c6cc3a 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -5,7 +5,7 @@ <% end %>
-<%= form_for(@node) do |f| %> + <%= form_for(@node, html: { data: { autosave_url: autosave_node_path(@node), show_url: node_path(@node) } }) do |f| %> <% if @node.errors.any? %>
    <% @node.errors.full_messages.each do |msg| %>
  • <%= msg %>
  • <% end %>
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 41699a1..4107ce8 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -159,13 +159,45 @@ cccms = { page.cached_abstract = elements.abstract.val(); page.cached_body = elements.body.html(); - $("#flash").append(""); - $.post(this.attr("action"), $(this).serialize(), null, "script"); + var data = this.serializeArray().filter(function(field) { + return field.name !== "_method"; + }); + data.push({ name: "_method", value: "put" }); + + $.ajax({ + type: "POST", + url: this.attr("data-autosave-url"), + data: data, + error: function(xhr) { + if (xhr.status === 423) { + clearInterval(cccms.autosave_timer); + cccms.report_lock_lost($("#page_editor > form").attr("data-show-url")); + } + // any other failure: quietly retried on the next tick + } + }); } }; - setInterval('$("#page_editor > form").submitWithAjax()', 7000); + cccms.autosave_timer = setInterval(function() { + $("#page_editor > form").submitWithAjax(); + }, 7000); + }, + + report_lock_lost : function(show_url) { + var $flash = $("#flash"); + if ($flash.length === 0) { + $flash = $("
", { id: "flash" }).insertAfter(".admin_content_spacer"); + } + + var $banner = $("", { "class": "warning" }).text( + "This page is now locked by someone else — your changes here can no longer be saved. Copy anything you need, then " + ); + $banner.append($("", { href: show_url }).text("return to the published page")); + $banner.append("."); + + $flash.html($banner); } } diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 0c0779f..7ae374c 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -200,6 +200,11 @@ span#flash_error, span.warning { padding-bottom: 1px; } +span.warning a { + color: inherit; + text-decoration: underline; +} + /* ============================================================ Pagination ============================================================ */ -- cgit v1.3 From 6398042d3e009d54b15ad01faf993b3d97a203c6 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 17:33:26 +0200 Subject: Add a action bar to the node edit view. This is the first application of a proposed new layout --- app/views/nodes/edit.html.erb | 45 +++++++++++++++++++++++++++++-------------- public/stylesheets/admin.css | 8 ++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 1c6cc3a..feba92a 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -1,9 +1,3 @@ -<% content_for :subnavigation do %> - <%= 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?" }, class: 'button_to state_changing' } %> -<% end %> -
<%= form_for(@node, html: { data: { autosave_url: autosave_node_path(@node), show_url: node_path(@node) } }) do |f| %> <% if @node.errors.any? %> @@ -14,6 +8,23 @@

<%= 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)
@@ -38,12 +49,12 @@ %>
- <%= fields_for @draft do |d| %> + <%= fields_for @page do |d| %>
Tags - comma seperated
-
<%= text_field_tag :tag_list, @draft.tag_list.join(', ') %>
+
<%= text_field_tag :tag_list, @page.tag_list.join(', ') %>
Publish at
-
<%= d.datetime_select :published_at, :value => @draft.published_at %>
+
<%= d.datetime_select :published_at, :value => @page.published_at %>
Template
@@ -52,12 +63,15 @@
Author
-
<%= d.select :user_id, user_list %>
+
+ <%= d.select :user_id, user_list, + :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> +
Images
-
    - <% @draft.assets.images.each do |image| %> +
      + <% @page.assets.images.each do |image| %>
    • <%= image_tag(image.upload.url(:thumb)) %>
    • @@ -86,8 +100,11 @@
      Body
      <%= d.text_area :body, :class => 'with_editor' %>
      + <% end %> -
      <%= d.submit 'save' %>
      +
      + <%= f.submit 'Save Draft' %> + <%= f.submit 'Save + Unlock + Exit' %> +
      <% end %> -<% end %>
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 7ae374c..ceacf17 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -165,6 +165,14 @@ input[type=radio] { margin-bottom: 40px; } +#node_action_bar { + margin-bottom: 1rem; +} + +#node_action_bar > * { + margin-right: 0.5rem; +} + /* ============================================================ Flash / notices ============================================================ */ -- 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/edit.html.erb') diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index d8586e2..72d4a3e 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 feba92a..cdc9b36 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 2ab7986..036caf2 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 ceacf17..a6e8bf6 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -79,6 +79,9 @@ input[type=radio] { #wrapper { margin: 0 125px; } + .node_action_bar.node_action_bar_save { + margin-left: 120px; + } } @media(max-width:1015px) { #wrapper { @@ -165,12 +168,14 @@ input[type=radio] { margin-bottom: 40px; } -#node_action_bar { - margin-bottom: 1rem; -} - -#node_action_bar > * { - margin-right: 0.5rem; +.node_action_bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin: 1rem 0 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e8e8e8; } /* ============================================================ @@ -213,6 +218,18 @@ span.warning a { text-decoration: underline; } +.error_messages { + border-left: 3px solid #cc0000; + background-color: #fdecea; + padding: 6px 10px; + margin-bottom: 1rem; +} + +.error_messages ul { + margin: 0; + padding-left: 1.25rem; +} + /* ============================================================ Pagination ============================================================ */ @@ -487,6 +504,16 @@ table.user_table td.user_login { padding: 0.5rem 0.75rem; } +.node_info_group .disabled_action { + display: inline-block; + border: 1px solid #c0c0c0; + border-radius: 2px; + padding: 4px 12px; + font-weight: bold; + color: #969696; + cursor: not-allowed; +} + .node_info_group_items { display: flex; flex-wrap: wrap; @@ -514,6 +541,38 @@ table.user_table td.user_login { margin-bottom: 0.25rem; } +.node_content.node_status { + border: 2px solid #000000; + background-color: #fafafa; +} + +.node_status .node_info_group_items:first-child { + margin-bottom: 0.75rem; +} + +.node_status .node_info_group_items + .node_info_group_items { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.node_status .field_hint { + margin-top: 0.5rem; + display: block; +} + +#page_editor a.action_button, +.node_status form.button_to input[type="submit"], +.node_status form.button_to button[type="submit"] { + padding: 4px 12px; + line-height: 1.2; + box-sizing: border-box; +} + +.node_status form.button_to input[type="submit"], +.node_status form.button_to button[type="submit"] { + border: 1px solid transparent; +} + .field_hint { display: block; margin-top: 2px; @@ -562,6 +621,23 @@ div#page_editor { margin-left: 10px; } +#page_editor a.action_button { + display: inline-block; + -webkit-appearance: none; + appearance: none; + border: 1px solid #000000; + border-radius: 2px; + padding: 4px 12px; + font-weight: bold; + text-decoration: none; + color: #000000; +} + +#page_editor a.action_button:hover { + color: #ffffff; + background-color: #000000; +} + @media(min-width:1016px) { input#tag_list, input#node_staged_slug, -- cgit v1.3 From 205e6216fc7850fe717122c189e5003d1f9e8afe Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 02:03:19 +0200 Subject: Add head/draft/autosave layer comparison at three UI entry points Node#resolve_page_reference and #available_layer_pairs let Page#diff_against compare named layers (head/draft/autosave), not just numbered revisions -- autosave was never part of Node#pages, so this was the missing piece. Wired into nodes#show's Status section, nodes#edit right after an autosave gets resurrected ("What changed?"), and the admin wizard's current-drafts table, which now also lists autosave-only nodes it previously never showed. revisions#diff hides the numbered-revision picker when comparing named layers (it can't represent them), shows a plain label instead, and offers buttons to switch between whichever other pairs make sense for the node's current state. Destroying the topmost layer is available directly from the diff view, reusing the existing revert! path. "Discard changes" is renamed "Discard Autosave" everywhere it appears, to match "Destroy Draft". --- app/controllers/admin_controller.rb | 4 +-- app/controllers/nodes_controller.rb | 2 +- app/controllers/revisions_controller.rb | 16 ++++++++--- app/helpers/revisions_helper.rb | 5 ++++ app/models/node.rb | 21 ++++++++++++++ app/views/admin/index.html.erb | 13 +++++++-- app/views/nodes/edit.html.erb | 9 +++++- app/views/nodes/show.html.erb | 12 +++++++- app/views/revisions/diff.html.erb | 40 +++++++++++++++++++++++---- config/routes.rb | 1 + test/controllers/admin_controller_test.rb | 15 ++++++++-- test/controllers/revisions_controller_test.rb | 39 ++++++++++++++++++++++++++ test/models/node_test.rb | 22 +++++++++++++++ 13 files changed, 179 insertions(+), 20 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 3fa0519..6ab2135 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -5,10 +5,10 @@ class AdminController < ApplicationController before_action :login_required def index - @drafts = Node.where("draft_id IS NOT NULL") + @drafts = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL") .limit(50).order("updated_at desc") - @drafts_count = Node.where("draft_id IS NOT NULL").count + @drafts_count = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL").count @recent_changes = Node.where( "updated_at < ? AND updated_at > ? AND parent_id IS NOT NULL", diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 38d42d9..d1538e1 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -72,7 +72,7 @@ class NodesController < ApplicationController if @node.autosave flash.now[:notice] = "This page has unsaved changes from a previous session, shown below. " \ - "Save to keep them, or use \"Discard changes\" below to go back to the last saved version." + "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version." elsif freshly_locked flash.now[:notice] = "Node locked and ready to edit" end diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb index 9acb26f..4b0c549 100644 --- a/app/controllers/revisions_controller.rb +++ b/app/controllers/revisions_controller.rb @@ -21,10 +21,18 @@ class RevisionsController < ApplicationController params[:start_revision], params[:end_revision] = 1, 1 end - @start = @node.pages.find_by_revision( params[:start_revision] ) - @end = @node.pages.find_by_revision( params[:end_revision] ) - @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline - @diff = @end.diff_against( @start, view: @diff_view ) + @start = @node.resolve_page_reference(params[:start_revision]) + @end = @node.resolve_page_reference(params[:end_revision]) + + if @start.nil? || @end.nil? + flash[:error] = "That comparison is no longer available." + redirect_to(node_path(@node)) and return + end + + @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline + @diff = @end.diff_against(@start, view: @diff_view) + @available_layer_pairs = @node.available_layer_pairs + @locked_by_other = @node.locked? && @node.lock_owner != current_user end def show diff --git a/app/helpers/revisions_helper.rb b/app/helpers/revisions_helper.rb index fdb51f8..a629013 100644 --- a/app/helpers/revisions_helper.rb +++ b/app/helpers/revisions_helper.rb @@ -1,2 +1,7 @@ module RevisionsHelper + # Human-readable label for a diff endpoint -- "head"/"draft"/"autosave" + # get their name; anything else is a revision number. + def describe_page_reference(ref) + %w[head draft autosave].include?(ref.to_s) ? ref.to_s.capitalize : "revision #{ref}" + end end diff --git a/app/models/node.rb b/app/models/node.rb index 7675ab6..82d9954 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -123,6 +123,27 @@ class Node < ApplicationRecord self.draft.reload end + def resolve_page_reference ref + case ref.to_s + when "head" then head + when "draft" then draft + when "autosave" then autosave + else pages.find_by_revision(ref) + end + end + + # Which layer-pairs are meaningful to compare right now, given this + # node's actual state. Head vs autosave only shows up when no draft + # sits between them -- with a draft present, autosave is compared + # against the draft, never past it straight to head. + def available_layer_pairs + pairs = [] + pairs << [:head, :draft] if head && draft + pairs << [:draft, :autosave] if draft && autosave + pairs << [:head, :autosave] if head && autosave && !draft + pairs + end + def find_or_create_draft current_user self.wipe_draft! if draft && self.lock_owner == current_user diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 77b45f4..c67ccb3 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -82,9 +82,9 @@
- -

Current Drafts (<%= @drafts_count %>)

- + +

Current Drafts & Autosaves (<%= @drafts_count %>)

+ @@ -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 cdc9b36..1c19410 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -6,9 +6,16 @@ disabled: @node.autosave.present? %> <% if @node.autosave || (@node.draft && @node.head) %> - <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'), + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), revert_node_path(@node), method: :put, form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %> + <% if pair = @node.available_layer_pairs.find { |p| p.include?(:autosave) } %> + <%= button_to 'What changed?', + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last }, + form: { class: 'button_to computation' } %> + <% end %> <% end %> <%= submit_tag "Save Draft", form: dom_id(@node, :edit) %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 036caf2..2469310 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -32,6 +32,16 @@ <% end %>
+ <% @node.available_layer_pairs.each do |pair| %> +
+ <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}", + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last }, + form: { class: 'button_to computation' } %> +
+ <% end %> + <% unless locked_by_other %> <% if @node.draft && !@node.autosave %>
@@ -41,7 +51,7 @@ <% end %> <% if @node.draft || @node.autosave %>
- <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'), + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), revert_node_path(@node), method: :put, form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index d7bb528..3157dca 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -4,11 +4,41 @@ <%= link_to 'Revisions', node_revisions_path(@node) %>

-<%= form_tag diff_node_revisions_path do %> - <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> - <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> - <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> - <%= submit_tag 'Diff' %> +

+ Comparing <%= describe_page_reference(params[:start_revision]) %> + against <%= describe_page_reference(params[:end_revision]) %> +

+ +<% numeric_comparison = params[:start_revision].to_s =~ /\A\d+\z/ && params[:end_revision].to_s =~ /\A\d+\z/ %> + +<% if numeric_comparison %> + <%= form_tag diff_node_revisions_path do %> + <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> + <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> + <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> + <%= submit_tag 'Diff' %> + <% end %> +<% else %> +

<%= link_to 'Compare two numbered revisions instead', node_revisions_path(@node) %>

+<% end %> + +<% if @available_layer_pairs.present? %> +

+ <% @available_layer_pairs.each do |pair| %> + <% next if [params[:start_revision].to_s, params[:end_revision].to_s].sort == pair.map(&:to_s).sort %> + <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}", + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last, view: @diff_view }, + form: { class: 'button_to computation' } %> + <% end %> + + <% if !@locked_by_other && (@node.autosave || @node.draft) %> + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), + revert_node_path(@node), method: :put, + form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %> + <% end %> +

<% end %>
diff --git a/config/routes.rb b/config/routes.rb index c0aef2f..da6b626 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,7 @@ Cccms::Application.routes.draw do resources :revisions do collection do post :diff + get :diff end member do put :restore diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 9bbf29b..d6005ba 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -1,8 +1,17 @@ require 'test_helper' class AdminControllerTest < ActionController::TestCase - # Replace this with your real tests. - test "the truth" do - assert true + test "current drafts includes nodes with only an autosave" do + node = Node.root.children.create!(:slug => "admin_autosave_only") + node.lock_for_editing!(User.find_by_login("aaron")) + node.autosave!({title: "in progress"}, User.find_by_login("aaron")) + node.save_draft!(User.find_by_login("aaron")) + node.publish_draft! + node.lock_for_editing!(User.find_by_login("aaron")) + node.autosave!({title: "editing again"}, User.find_by_login("aaron")) + + login_as :quentin + get :index + assert_includes assigns(:drafts), node end end diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index caca6bf..162e6f1 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -100,4 +100,43 @@ class RevisionsControllerTest < ActionController::TestCase assert_response :success assert_select ".diff_column", 2 end + + test "diffing head against draft by name" do + login_as :quentin + @node.find_or_create_draft(@user) + @node.draft.update(:body => "draft body") + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + end + + test "diffing a layer pair that no longer exists redirects with a flash" do + login_as :quentin + post(:diff, params: { :node_id => @node.id, :start_revision => "draft", :end_revision => "autosave" }) + assert_redirected_to node_path(@node) + assert flash[:error].present? + end + + test "diffing by name shows a clear comparison label instead of a misleading revision picker" do + login_as :quentin + @node.find_or_create_draft(@user) + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + assert_select "strong", "Head" + assert_select "strong", "Draft" + assert_select "select[name=?]", "start_revision", :count => 0 + end + + test "pair-switcher buttons carry their params as real hidden fields, not a query string" do + login_as :quentin + @node.find_or_create_draft(@user) + @node.lock_for_editing!(@user) + @node.autosave!({ :body => "unsaved" }, @user) + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + assert_select "form.computation input[type=hidden][name=start_revision]" + assert_select "form.computation input[type=hidden][name=end_revision]" + end end diff --git a/test/models/node_test.rb b/test/models/node_test.rb index 2138c19..9e71dec 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb @@ -473,4 +473,26 @@ class NodeTest < ActiveSupport::TestCase node.publish_draft! end end + + test "available_layer_pairs matches the six-state table" do + node = Node.root.children.create!(:slug => "layer_pairs_test") + user = @user1 || User.find_by_login("aaron") + + assert_equal [[:draft, :autosave]], (node.lock_for_editing!(user); node.autosave!({title: "v1"}, user); node.available_layer_pairs) # state F + + node.save_draft!(user) + node.publish_draft! + assert_equal [], node.available_layer_pairs # state A + + node.lock_for_editing!(user) + node.autosave!({title: "v2"}, user) + assert_equal [[:head, :autosave]], node.available_layer_pairs # state B + + node.save_draft!(user) + assert_equal [[:head, :draft]], node.available_layer_pairs # state C + + node.lock_for_editing!(user) + node.autosave!({title: "v3"}, user) + assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D + end end -- cgit v1.3 From 19e0ee821d5b2b6d3397a81411f4f3a4cbd2fa83 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 12 Jul 2026 13:58:36 +0200 Subject: Unify the four field-level pickers, fix search visibility menu_items/parent_search/move_to_search/event_search each duplicated their own debounce-free AJAX call and result rendering. Replaced with one shared initSearchPicker, taking each picker's distinctive behavior (parent_search's live path preview, event_search's title hint) as a callback rather than a whole reimplementation. Adds a real debounce with an out-of-order-response guard -- none of the four had either before. admin_search (the Alt+F top-bar search) now shares the same function via its own url/isActive/header options, gaining the same guard and fixing an inconsistency of its own (it previously always slid its panel open, even on zero results). Each picker's results container gets its own id instead of sharing was ever supposed to be on screen at once, an assumption with no actual enforcement behind it. Styling moved from an id-pair (#menu_search_results, #search_results) to a shared .search_results class so a future picker never needs this file touched again. menu_search and the admin top-bar search now call Node.editor_search instead of the public, head-only Node.search -- both are admin-only, authenticated views, and had no reason to inherit the public search's "can't find a draft" limitation. The always-ignored :per_page => 1000 on the latter is gone too; Node.search's second argument was never read by the method at all. Also removed a stale #metadata a { text-transform: lowercase } rule, found while verifying the above -- written for the pre-subnav-removal expand-toggle, which no longer exists; it had been silently lowercasing nodes#edit's own, unrelated #metadata div (including move_to_search's results) by id coincidence ever since. #main_navigation and #overview_toggle intentionally left capitalized rather than special-cased -- both belong to the nav bar already slated to shrink to three icons, not worth polishing on the way out. --- app/controllers/admin_controller.rb | 4 +- app/views/events/edit.html.erb | 4 +- app/views/events/new.html.erb | 2 +- app/views/layouts/admin.html.erb | 4 +- app/views/menu_items/edit.html.erb | 2 +- app/views/menu_items/new.html.erb | 4 +- app/views/nodes/edit.html.erb | 14 +- app/views/nodes/new.html.erb | 2 +- public/javascripts/admin_search.js | 355 ++++++++++++------------------------ public/stylesheets/admin.css | 13 +- 10 files changed, 137 insertions(+), 267 deletions(-) (limited to 'app/views/nodes/edit.html.erb') diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 435df57..3c45c49 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -31,7 +31,7 @@ class AdminController < ApplicationController end def search - @results = Node.search params[:search_term], :per_page => 1000 + @results = Node.editor_search(params[:search_term]) respond_to do |format| format.html do @@ -53,7 +53,7 @@ class AdminController < ApplicationController if params[:search_term] == "Root" @results = [Node.root] else - @results = Node.search params[:search_term] + @results = Node.editor_search(params[:search_term]) end respond_to do |format| diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb index 6cb04bd..45b084f 100644 --- a/app/views/events/edit.html.erb +++ b/app/views/events/edit.html.erb @@ -19,12 +19,12 @@
Change node <%= text_field_tag :event_node_search_term %> -
+
This will re-link the event to a different node — rarely needed.
<% else %> <%= text_field_tag :event_node_search_term %> -
+
Optional — search and pick a node to associate this event with a page. <% end %> <%= f.hidden_field :node_id %> diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb index b20fe48..7a1ee7a 100644 --- a/app/views/events/new.html.erb +++ b/app/views/events/new.html.erb @@ -10,7 +10,7 @@
Node
<%= text_field_tag :event_node_search_term %> -
+
<%= f.hidden_field :node_id %> Optional — search and pick a node to associate this event with a page.
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index e714c28..079346b 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -56,9 +56,7 @@ Search: <%= text_field_tag :search_term, nil, autocomplete: "off" %> <% end %>
- +
diff --git a/app/views/menu_items/edit.html.erb b/app/views/menu_items/edit.html.erb index dc5e8f9..44e5a79 100644 --- a/app/views/menu_items/edit.html.erb +++ b/app/views/menu_items/edit.html.erb @@ -6,7 +6,7 @@
Search
<%= text_field_tag :menu_search_term %> -
+
Node Id
diff --git a/app/views/menu_items/new.html.erb b/app/views/menu_items/new.html.erb index 68081d0..223cb8e 100644 --- a/app/views/menu_items/new.html.erb +++ b/app/views/menu_items/new.html.erb @@ -7,9 +7,7 @@ Search <%= text_field_tag :menu_search_term %> -
- -
+ diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 1c19410..13b78fc 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -45,14 +45,12 @@
parent
<%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> -
- -
- <%= f.hidden_field( - :staged_parent_id, - :value => @node.staged_parent_id || @node.parent_id - ) - %> +
+ <%= f.hidden_field( + :staged_parent_id, + :value => @node.staged_parent_id || @node.parent_id + ) + %>
<%= fields_for @page do |d| %> diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index afc632f..bb7e078 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb @@ -34,7 +34,7 @@
<%= text_field_tag :parent_search_term, @parent_name %> <%= hidden_field_tag :parent_id, @parent_id %> -
+
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 0e70845..f553334 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -1,5 +1,4 @@ admin_search = { - initialize : function() { $(document).bind("keydown", 'Alt+f', function(){ admin_search.display_toggle(); @@ -20,25 +19,12 @@ admin_search = { } }); - $("#search_term").bind("input", function() { - if (!$('#search_widget').is(':visible')) return; - if ($(this).val()) { - $.ajax({ - type: "GET", - url: ADMIN_SEARCH_URL, - data: "search_term=" + $(this).val(), - dataType: "json", - success: function(results) { - admin_search.show_results(results); - }, - error: function(xhr, status, error) { - console.log("Ajax error:", status, error, xhr.status, xhr.responseText); - } - }); - } else { - $('#menu_search_results').slideUp(); - $('#menu_search_results').empty(); - } + initSearchPicker({ + inputSelector: "#search_term", + resultsSelector: "#menu_search_results", + url: ADMIN_SEARCH_URL, + isActive: function() { return $('#search_widget').is(':visible'); }, + resultsHeaderHtml: "

Press Enter to see all results ⏎

" }); }, @@ -49,75 +35,97 @@ admin_search = { $('#search_widget').fadeIn(); $('#search_term').focus(); } - }, - - show_results : function(results) { - $('#menu_search_results').empty(); - if (results.length) { - $('#menu_search_results').append( - "

Press Enter to see all results ⏎

" - ); - } - for (result in results) { - $('#menu_search_results').append( - "

" + - results[result].title + - "" + results[result].unique_name + "" + - "

" - ); - } - $('#menu_search_results').slideDown(); } }; -menu_items = { +function initSearchPicker(options) { + var inputSelector = options.inputSelector; + var resultsSelector = options.resultsSelector; + var url = options.url || ADMIN_MENU_SEARCH_URL; + var onSelect = options.onSelect; // omit for a real-navigation search like admin_search + var isActive = options.isActive; // optional guard, checked before every search + var resultsHeaderHtml = options.resultsHeaderHtml; // optional, prepended only when results exist + var requestId = 0; + var timeout; + + $(inputSelector).bind("input", function() { + if (isActive && !isActive()) return; + + var term = $(this).val(); + var results = $(resultsSelector); + clearTimeout(timeout); + + if (!term) { + results.slideUp(); + results.empty(); + return; + } - initialize_search : function() { - $("#menu_search_term").bind("input", function() { - if ($(this).val()) { - $.ajax({ - type: "GET", - url: ADMIN_MENU_SEARCH_URL, - data: "search_term=" + $(this).val(), - dataType: "json", - success : function(results) { - menu_items.show_results(results); + timeout = setTimeout(function() { + var thisRequest = ++requestId; + $.ajax({ + type: "GET", + url: url, + data: "search_term=" + term, + dataType: "json", + success: function(nodes) { + if (thisRequest !== requestId) return; + results.empty(); + if (nodes.length && resultsHeaderHtml) { + results.append(resultsHeaderHtml); } - }); - } - else { - $('#search_results').slideUp(); - $('#search_results').empty(); + var found = false; + for (var i = 0; i < nodes.length; i++) { + (function(node) { + var link; + if (onSelect) { + link = $( + "

" + node.title + + "" + node.unique_name + "" + + "

" + ); + link.find("a").bind("click", function() { + onSelect(node); + results.slideUp(); + results.empty(); + return false; + }); + } else { + link = $( + "

" + node.title + + "" + node.unique_name + "" + + "

" + ); + } + results.append(link); + found = true; + })(nodes[i]); + } + if (found) { + results.slideDown(); + } else { + results.slideUp(); + } + }, + error: function(xhr, status, error) { + console.log("Ajax error:", status, error, xhr.status, xhr.responseText); + } + }); + }, 250); + }); +} + +menu_items = { + initialize_search : function() { + initSearchPicker({ + inputSelector: "#menu_search_term", + resultsSelector: "#menu_item_search_results", + onSelect: function(node) { + $("#menu_item_node_id").val(node.node_id); + $("#menu_item_path").val("/" + node.unique_name); + $("#menu_item_title").val(node.title); } }); - }, - - show_results : function(results) { - $("#search_results").empty(); - for (result in results) { - var link = $((""+ results[result].title + "")); - $(link).bind("click", menu_items.link_closure(results[result])); - - - // Sometimes I don't get jquery; wrap() didn't work *sigh* - // Guess I'll need a book someday or another framework - var wrapper = $("
"); - $(wrapper).append(link) - - $("#search_results").append(wrapper); - - } - }, - - link_closure : function(node) { - var barf = function(){ - $("#menu_item_node_id").val(node.node_id); - $("#menu_item_path").val("/" + node.unique_name); - $("#menu_item_title").val(node.title); - return false; - } - - return barf; } }; @@ -125,21 +133,13 @@ parent_search = { initialize_search : function() { parent_search.initialize_radio_buttons(); - $("#parent_search_term").bind("input", function() { - if ($(this).val()) { - $.ajax({ - type: "GET", - url: ADMIN_MENU_SEARCH_URL, - data: "search_term=" + $(this).val(), - dataType: "json", - success : function(results) { - parent_search.show_results(results); - } - }); - } - else { - $('#search_results').slideUp(); - $('#search_results').empty(); + initSearchPicker({ + inputSelector: "#parent_search_term", + resultsSelector: "#parent_search_results", + onSelect: function(node) { + $("#parent_search_term").val(node.title); + $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name); + parent_search.update_resulting_path(); } }); @@ -154,38 +154,6 @@ parent_search = { }); }, - show_results : function(results) { - $("#search_results").empty(); - var found = false; - for (result in results) { - - var link = $(( - "

" + results[result].title + - "" + results[result].unique_name + "" + - "

")); - - $(link).bind("click", parent_search.link_closure(results[result])); - - $("#search_results").append(link); - found = true; - } - if (found) - $('#search_results').slideDown(); - }, - - link_closure : function(node) { - var barf = function(){ - $("#parent_search_term").val(node.title); - $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name); - $('#search_results').slideUp(); - $('#search_results').empty(); - parent_search.update_resulting_path(); - return false; - } - - return barf; - }, - update_resulting_path : function() { var kind = $("input[name='kind']:checked"); var title = $("#title").val(); @@ -223,122 +191,35 @@ parent_search = { $("#parent_search_field").hide(); } } -} +}; move_to_search = { initialize_search : function() { - $("#move_to_search_term").bind("input", function() {move_to_search.do_search($(this))}); - }, - - do_search : function(_this) { - if (_this.val()) { - $.ajax({ - type: "GET", - url: ADMIN_MENU_SEARCH_URL, - data: "search_term=" + _this.val(), - dataType: "json", - success : function(results) { - move_to_search.show_results(results); - } - }); - } - else { - $('#search_results').slideUp(); - $('#search_results').empty(); - } - }, - - show_results : function(results) { - $("#search_results").empty(); - var found = false; - for (result in results) { - var link = $((""+ results[result].title + "")); - $(link).bind("click", move_to_search.link_closure(results[result])); - - - // Sometimes I don't get jquery; wrap() didn't work *sigh* - // Guess I'll need a book someday or another framework - var wrapper = $("
"); - $(wrapper).append(link) - - $("#search_results").append(wrapper); - found = true; - } - if (found) - $('#search_results').slideDown(); - else - $('#search_results').slideUp(); - - }, - - link_closure : function(node) { - var barf = function(){ - $("#move_to_search_term").val(node.title); - $("#node_staged_parent_id").val(node.node_id); - $('#search_results').slideUp(); - $('#search_results').empty(); - return false; - } - - return barf; + initSearchPicker({ + inputSelector: "#move_to_search_term", + resultsSelector: "#move_to_search_results", + onSelect: function(node) { + $("#move_to_search_term").val(node.title); + $("#node_staged_parent_id").val(node.node_id); + } + }); } -} +}; event_search = { initialize_search : function() { - $("#event_node_search_term").bind("input", function() { - if ($(this).val()) { - $.ajax({ - type: "GET", - url: ADMIN_MENU_SEARCH_URL, - data: "search_term=" + $(this).val(), - dataType: "json", - success : function(results) { - event_search.show_results(results); - } - }); - } - else { - $('#search_results').slideUp(); - $('#search_results').empty(); + initSearchPicker({ + inputSelector: "#event_node_search_term", + resultsSelector: "#event_search_results", + onSelect: function(node) { + $("#event_node_search_term").val(node.title); + $("#event_node_id").val(node.node_id); + + var title_field = $("#event_title"); + if (title_field.val() === "") { + $("#event_title_hint").text("Using \"" + node.title + "\" from the associated node."); + } } }); - }, - - show_results : function(results) { - $("#search_results").empty(); - var found = false; - for (result in results) { - var link = $(( - "

" + results[result].title + - "" + results[result].unique_name + "" + - "

")); - - $(link).bind("click", event_search.link_closure(results[result])); - - $("#search_results").append(link); - found = true; - } - if (found) - $('#search_results').slideDown(); - else - $('#search_results').slideUp(); - }, - - link_closure : function(node) { - var barf = function(){ - $("#event_node_search_term").val(node.title); - $("#event_node_id").val(node.node_id); - $('#search_results').slideUp(); - $('#search_results').empty(); - - var title_field = $("#event_title"); - if (title_field.val() === "") { - $("#event_title_hint").text("Using \"" + node.title + "\" from the associated node."); - } - - return false; - } - return barf; } }; diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 5c1e489..da31535 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -155,7 +155,6 @@ input[type=radio] { padding-right: 5px; padding-top: 1px; padding-bottom: 1px; - text-transform: lowercase; } #metadata a:hover { @@ -1023,22 +1022,19 @@ div#draft_list table td.actions a { font-size: 18px; } -#menu_search_results p, -#search_results p { +.search_results p { margin: 0; padding: 4px 4px 0 4px; border-bottom: 1px solid #e8e8e8; } -#menu_search_results p a, -#search_results p a { +.search_results p a { display: block; font-weight: bold; font-size: 0.95rem; } -#menu_search_results p span.result_path, -#search_results p span.result_path { +.search_results p span.result_path { display: block; margin-top: 2px; font-size: 0.75rem; @@ -1047,8 +1043,7 @@ div#draft_list table td.actions a { padding-bottom: 4px; } -#menu_search_results p.search_more, -#search_results p.search_more { +.search_results p.search_more { margin: 0; padding: 6px 4px; font-size: 0.8rem; -- 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/edit.html.erb') diff --git a/app/views/assets/index.html.erb b/app/views/assets/index.html.erb index 6591091..8c35561 100644 --- a/app/views/assets/index.html.erb +++ b/app/views/assets/index.html.erb @@ -1,6 +1,8 @@

Assets

-<%= link_to 'New asset', new_asset_path, class: 'action_button' %> +<%= link_to new_asset_path, class: 'action_button' do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create asset +<% end %> <%= will_paginate @assets %> @@ -20,7 +22,9 @@ <%= asset.upload.content_type %> <%= link_to 'Show', asset %> <%= link_to 'Edit', edit_asset_path(asset) %> - <%= button_to 'Destroy', asset, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } %> + <%= button_to asset, method: :delete, form: { data: { confirm: 'Are you sure?' }, class: 'button_to destructive' } do %> + <%= icon("trash", library: "tabler", "aria-hidden": true) %> Destroy + <% end %> <% end %> diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb index 0286678..5717dd9 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 45b084f..b6564a4 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 3d953d5..dde897e 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 5812228..ac084c9 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 52514ab..bb27173 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 e84199b..591eb68 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 03f38b4..f7210f1 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 939f19c..543bc53 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 13b78fc..1ac05e0 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 2688ea1..3bb251c 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 07cb5d1..6e20258 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 82818d0..aa084ed 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 05c7311..da67871 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 b9ce6bd..5638b7a 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 798b82b..3a88f5e 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 46e958b..4f5266d 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 e744efe..70881db 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 5cd5ad4..58f8a86 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 8baac265059b70da0148487458ee4077b15f155e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 04:54:33 +0200 Subject: Asset picker: attach/detach/reorder UI, read-only view, autosave fix Replaces nodes#edit's old Images section -- a hidden panel dumping every image asset in the system unfiltered (#image_browser) plus a raw drag-and-drop box (#image_box) -- with a small search-and-click picker built on the endpoint from the last two commits. Attaching posts immediately and appends the new thumbnail via a cloned
Login