From a8cf725312d245d8d1f1df0fff22718975ce249e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 20 Aug 2026 12:50:30 +0200 Subject: Add redirect ui in in nodes#edit and apply some minor cleanup Also give search inputs a magnifier and a clear control: input_group wraps a field with an icon and, where clearing is meaningful, a button that empties it and any hidden companion named by data-clears. Clean up some inconistency between field hints presented as p and as span. Remove stray console.log. --- app/controllers/content_controller.rb | 5 ++++ app/models/page.rb | 26 ++++++++++------- app/views/nodes/edit.html.erb | 53 ++++++++++++++++++++++++++--------- config/locales/de.yml | 5 ++++ config/locales/en.yml | 5 ++++ public/javascripts/admin_interface.js | 27 +++++++++++++++++- public/javascripts/admin_search.js | 14 +++++++++ public/stylesheets/admin.css | 45 +++++++++++++++++++++++++---- 8 files changed, 150 insertions(+), 30 deletions(-) diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 8be4bfbd..debb69b9 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -14,6 +14,11 @@ class ContentController < ApplicationController expires_in 20.minutes, :public => true if @page and @page.public? + if (target = @page.redirect_target) + return redirect_to(target.internal? ? content_path(target.node.unique_name) : target.url, + :status => @page.redirect_status) + end + render( :template => @page.valid_template, :layout => true diff --git a/app/models/page.rb b/app/models/page.rb index 9115ccb0..b17708e7 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -124,7 +124,7 @@ class Page < ApplicationRecord end # One row per non-default locale, read from the actual translation - # row -- never through the locale-dependent accessor, so a locale + # row, never through the locale-dependent accessor, so a locale # with no real translation yet reports as absent rather than quietly # showing a fallback value borrowed from another locale. def translation_summary @@ -311,21 +311,27 @@ class Page < ApplicationRecord published_at.nil? ? true : published_at < Time.now end - # The destination this page sends visitors to, or nil. An internal target - # wins over an external one. A target that is restricted or has no head is - # no destination at all, so the page renders itself rather than linking to - # nothing. The banner partial calls this too, so the precedence cannot - # drift between the redirect and the link. + # Where this page sends visitors, or nil. Internal wins over external. A + # node that is restricted or has no head is no destination at all, so the + # page renders itself rather than pointing at nothing. + RedirectTarget = Struct.new(:node, :url) do + def internal? + node.present? + end + end + def redirect_target return nil if redirect.blank? if redirect_node_id.present? - node = Node.find_by(:id => redirect_node_id) - return nil unless node&.head && !node.restricted? - return node.unique_name + target = Node.find_by(:id => redirect_node_id) + return nil unless target&.head && !target.restricted? + return RedirectTarget.new(target, nil) end - external_url.presence + return RedirectTarget.new(nil, external_url) if external_url.present? + + nil end def redirect_status diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 927cc648..f629985e 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -80,7 +80,7 @@ <% end %> -

<%= t(".headline_hint") %>

+ <%= t(".headline_hint") %> <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: t(".attach_search_placeholder"), autocomplete: "off" %> @@ -118,7 +118,7 @@
<%= t(".parent") %>
<%= text_field_tag :move_to_search_term, (Node.find_by(:id => @page.parent_node_id) || @node.parent)&.title %> -

<%= t(".parent_hint") %>

+ <%= t(".parent_hint") %>
<%= d.hidden_field( :parent_node_id, @@ -128,6 +128,43 @@
+
+
<%= t(".author") %>
+
+ <%= d.select :user_id, user_list, + :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> +
+
+ +
+
<%= t(".publish_at") %>
+
<%= d.datetime_select :published_at, :value => @page.published_at %>
+
+ +
+
<%= t(".redirect") %>
+
+ + <%= t(".redirect_hint") %> + +
"> +
+ <%= icon("search", library: "tabler", "aria-hidden": true) %> + <%= text_field_tag :redirect_search_term, + Node.find_by(:id => @page.redirect_node_id)&.title, + :placeholder => "—", :class => "clearable_input" %> +
+ + <%= d.hidden_field :redirect_node_id %> +
+
+
+
+
<%= t(".external_url") %>
@@ -143,11 +180,6 @@
-
-
<%= t(".publish_at") %>
-
<%= d.datetime_select :published_at, :value => @page.published_at %>
-
-
<%= t(".template") %>
@@ -156,13 +188,6 @@
-
-
<%= t(".author") %>
-
- <%= d.select :user_id, user_list, - :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> -
-
diff --git a/config/locales/de.yml b/config/locales/de.yml index e94dfb7e..95d6edc2 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -454,6 +454,11 @@ de: set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" remove_image_label: "Bild entfernen" external_url: "Externe Homepage" + redirect: "Weiterleitung" + redirect_enable: "Diese Seite leitet weiter" + redirect_hint: "Ohne Ziel-Node wird auf die externe Homepage weitergeleitet." + redirect_target_hint: "Bleibt erhalten, wenn die Weiterleitung abgeschaltet wird." + redirect_clear: "Ziel entfernen" show: status: "Status" head: "Head" diff --git a/config/locales/en.yml b/config/locales/en.yml index 54d973e2..8b67f0ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -406,6 +406,11 @@ en: set_headline_title: "Use this photo as the page's headline image" remove_image_label: "Remove image" external_url: "External homepage" + redirect: "Redirect" + redirect_enable: "This page redirects" + redirect_hint: "If the taget node is empty, the external url will be used." + redirect_target_hint: "Disabling does not clear the destination" + redirect_clear: "Clear redirect" show: status: "Status" head: "Head" diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 994d3698..ffebb694 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -63,7 +63,6 @@ $(document).ready(function () { search_toggle.addEventListener('click', function (e) { admin_search.display_toggle(); e.preventDefault(); - console.log("Toggle"); }); } @@ -124,6 +123,32 @@ $(document).ready(function () { }); } + document.querySelectorAll('.field_clear').forEach(function (btn) { + btn.addEventListener('click', function () { + var wrap = btn.closest('.input_group'); + wrap.querySelector('input[type=text]').value = ''; + var hidden = wrap.dataset.clears; + if (hidden) { document.getElementById(hidden).value = ''; } + }); + }); + + if (document.getElementById('redirect_search_term')) { + redirect_search.initialize_search(); + + var mode = document.getElementById('page_redirect'); + var field = document.getElementById('redirect_target_field'); + mode.addEventListener('change', function () { + field.style.display = mode.checked ? '' : 'none'; + }); + + var term = document.getElementById('redirect_search_term'); + term.addEventListener('input', function () { + if (term.value === '') { + document.getElementById('page_redirect_node_id').value = ''; + } + }); + } + var metadata_details = document.getElementById('metadata_details'); if (metadata_details) { var desktop_mq = window.matchMedia('(min-width: 1016px)'); diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index e20e511a..746cecc1 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -263,6 +263,20 @@ move_to_search = { } }; +redirect_search = { + initialize_search : function() { + initSearchPicker({ + inputSelector: "#redirect_search_term", + resultsSelector: "#redirect_search_results", + showRestricted: true, + onSelect: function(node) { + $("#redirect_search_term").val(node.title); + $("#page_redirect_node_id").val(node.node_id); + } + }); + } +}; + restore_search = { initialize_search : function() { initSearchPicker({ diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 923cac4b..63333d93 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -446,11 +446,8 @@ form.button_to.computation button[type="submit"]:disabled { pointer-events: none; } -/* Native (non-button_to) submit buttons -- Create/Save/Publish forms. - Lower specificity than form.button_to's own rule, so button_to forms - are correctly unaffected. If a button_to output ever loses its class - wrapper, it will silently fall through to this bordered style instead - of rendering as a plain link -- worth knowing, not necessarily fixing. */ +/* Native (non-button_to) submit buttons used for Create/Save/Publish + forms. */ input[type="submit"] { -webkit-appearance: none; appearance: none; @@ -470,6 +467,44 @@ input[type="submit"]:hover { background-color: var(--text); } +.input_group { + width: 100%; + position: relative; + display: inline-block; +} + +.input_group .field_search_icon { + position: absolute; + left: 0.5rem; + top: 55%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; +} + +.input_group .field_search_icon svg { + width: 1em; + height: 1em; +} + +.input_group input[type="text"].clearable_input { + padding-left: 2rem; + padding-right: 1.75rem; +} + +.field_clear { + position: absolute; + right: 0.25rem; + top: 50%; + transform: translateY(-50%); + appearance: none; + background: none; + border: 0; + color: var(--text-muted); + cursor: pointer; + line-height: 1; +} + /* ============================================================ Admin dashboard ============================================================ */ -- cgit v1.3