diff options
| -rw-r--r-- | app/controllers/content_controller.rb | 5 | ||||
| -rw-r--r-- | app/models/page.rb | 26 | ||||
| -rw-r--r-- | app/views/nodes/edit.html.erb | 53 | ||||
| -rw-r--r-- | config/locales/de.yml | 5 | ||||
| -rw-r--r-- | config/locales/en.yml | 5 | ||||
| -rw-r--r-- | public/javascripts/admin_interface.js | 27 | ||||
| -rw-r--r-- | public/javascripts/admin_search.js | 14 | ||||
| -rw-r--r-- | 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 | |||
| 14 | expires_in 20.minutes, :public => true | 14 | expires_in 20.minutes, :public => true |
| 15 | 15 | ||
| 16 | if @page and @page.public? | 16 | if @page and @page.public? |
| 17 | if (target = @page.redirect_target) | ||
| 18 | return redirect_to(target.internal? ? content_path(target.node.unique_name) : target.url, | ||
| 19 | :status => @page.redirect_status) | ||
| 20 | end | ||
| 21 | |||
| 17 | render( | 22 | render( |
| 18 | :template => @page.valid_template, | 23 | :template => @page.valid_template, |
| 19 | :layout => true | 24 | :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 | |||
| 124 | end | 124 | end |
| 125 | 125 | ||
| 126 | # One row per non-default locale, read from the actual translation | 126 | # One row per non-default locale, read from the actual translation |
| 127 | # row -- never through the locale-dependent accessor, so a locale | 127 | # row, never through the locale-dependent accessor, so a locale |
| 128 | # with no real translation yet reports as absent rather than quietly | 128 | # with no real translation yet reports as absent rather than quietly |
| 129 | # showing a fallback value borrowed from another locale. | 129 | # showing a fallback value borrowed from another locale. |
| 130 | def translation_summary | 130 | def translation_summary |
| @@ -311,21 +311,27 @@ class Page < ApplicationRecord | |||
| 311 | published_at.nil? ? true : published_at < Time.now | 311 | published_at.nil? ? true : published_at < Time.now |
| 312 | end | 312 | end |
| 313 | 313 | ||
| 314 | # The destination this page sends visitors to, or nil. An internal target | 314 | # Where this page sends visitors, or nil. Internal wins over external. A |
| 315 | # wins over an external one. A target that is restricted or has no head is | 315 | # node that is restricted or has no head is no destination at all, so the |
| 316 | # no destination at all, so the page renders itself rather than linking to | 316 | # page renders itself rather than pointing at nothing. |
| 317 | # nothing. The banner partial calls this too, so the precedence cannot | 317 | RedirectTarget = Struct.new(:node, :url) do |
| 318 | # drift between the redirect and the link. | 318 | def internal? |
| 319 | node.present? | ||
| 320 | end | ||
| 321 | end | ||
| 322 | |||
| 319 | def redirect_target | 323 | def redirect_target |
| 320 | return nil if redirect.blank? | 324 | return nil if redirect.blank? |
| 321 | 325 | ||
| 322 | if redirect_node_id.present? | 326 | if redirect_node_id.present? |
| 323 | node = Node.find_by(:id => redirect_node_id) | 327 | target = Node.find_by(:id => redirect_node_id) |
| 324 | return nil unless node&.head && !node.restricted? | 328 | return nil unless target&.head && !target.restricted? |
| 325 | return node.unique_name | 329 | return RedirectTarget.new(target, nil) |
| 326 | end | 330 | end |
| 327 | 331 | ||
| 328 | external_url.presence | 332 | return RedirectTarget.new(nil, external_url) if external_url.present? |
| 333 | |||
| 334 | nil | ||
| 329 | end | 335 | end |
| 330 | 336 | ||
| 331 | def redirect_status | 337 | 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 @@ | |||
| 80 | </li> | 80 | </li> |
| 81 | <% end %> | 81 | <% end %> |
| 82 | </ul> | 82 | </ul> |
| 83 | <p class="field_hint"><%= t(".headline_hint") %></p> | 83 | <span class="field_hint"><%= t(".headline_hint") %></span> |
| 84 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: t(".attach_search_placeholder"), autocomplete: "off" %> | 84 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: t(".attach_search_placeholder"), autocomplete: "off" %> |
| 85 | <div id="related_asset_search_results" class="search_results"></div> | 85 | <div id="related_asset_search_results" class="search_results"></div> |
| 86 | </div> | 86 | </div> |
| @@ -118,7 +118,7 @@ | |||
| 118 | <div class="layout_row_label"><%= t(".parent") %></div> | 118 | <div class="layout_row_label"><%= t(".parent") %></div> |
| 119 | <div class="layout_row_content"> | 119 | <div class="layout_row_content"> |
| 120 | <%= text_field_tag :move_to_search_term, (Node.find_by(:id => @page.parent_node_id) || @node.parent)&.title %> | 120 | <%= text_field_tag :move_to_search_term, (Node.find_by(:id => @page.parent_node_id) || @node.parent)&.title %> |
| 121 | <p class="field_hint"><%= t(".parent_hint") %></p> | 121 | <span class="field_hint"><%= t(".parent_hint") %></span> |
| 122 | <div id="move_to_search_results" class="search_results"></div> | 122 | <div id="move_to_search_results" class="search_results"></div> |
| 123 | <%= d.hidden_field( | 123 | <%= d.hidden_field( |
| 124 | :parent_node_id, | 124 | :parent_node_id, |
| @@ -129,6 +129,43 @@ | |||
| 129 | </div> | 129 | </div> |
| 130 | 130 | ||
| 131 | <div class="layout_row"> | 131 | <div class="layout_row"> |
| 132 | <div class="layout_row_label"><%= t(".author") %></div> | ||
| 133 | <div class="layout_row_content"> | ||
| 134 | <%= d.select :user_id, user_list, | ||
| 135 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 136 | </div> | ||
| 137 | </div> | ||
| 138 | |||
| 139 | <div class="layout_row"> | ||
| 140 | <div class="layout_row_label"><%= t(".publish_at") %></div> | ||
| 141 | <div class="layout_row_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> | ||
| 142 | </div> | ||
| 143 | |||
| 144 | <div class="layout_row"> | ||
| 145 | <div class="layout_row_label"><%= t(".redirect") %></div> | ||
| 146 | <div class="layout_row_content"> | ||
| 147 | <label> | ||
| 148 | <%= d.check_box :redirect, { :checked => @page.redirect.present? }, | ||
| 149 | "temporary", "" %> | ||
| 150 | <%= t(".redirect_enable") %> | ||
| 151 | </label> | ||
| 152 | <span class="field_hint"><%= t(".redirect_hint") %></span> | ||
| 153 | |||
| 154 | <div id="redirect_target_field" style="<%= "display: none;" unless @page.redirect.present? %>"> | ||
| 155 | <div class="input_group" data-clears="page_redirect_node_id"> | ||
| 156 | <span class="field_search_icon"><%= icon("search", library: "tabler", "aria-hidden": true) %></span> | ||
| 157 | <%= text_field_tag :redirect_search_term, | ||
| 158 | Node.find_by(:id => @page.redirect_node_id)&.title, | ||
| 159 | :placeholder => "—", :class => "clearable_input" %> | ||
| 160 | <div id="redirect_search_results" class="search_results"></div> | ||
| 161 | <button type="button" class="field_clear" aria-label="clear input">×</button> | ||
| 162 | <%= d.hidden_field :redirect_node_id %> | ||
| 163 | </div> | ||
| 164 | </div> | ||
| 165 | </div> | ||
| 166 | </div> | ||
| 167 | |||
| 168 | <div class="layout_row"> | ||
| 132 | <div class="layout_row_label"><%= t(".external_url") %></div> | 169 | <div class="layout_row_label"><%= t(".external_url") %></div> |
| 133 | <div class="layout_row_content"> | 170 | <div class="layout_row_content"> |
| 134 | <%= d.text_field :external_url %> | 171 | <%= d.text_field :external_url %> |
| @@ -144,11 +181,6 @@ | |||
| 144 | </div> | 181 | </div> |
| 145 | 182 | ||
| 146 | <div class="layout_row"> | 183 | <div class="layout_row"> |
| 147 | <div class="layout_row_label"><%= t(".publish_at") %></div> | ||
| 148 | <div class="layout_row_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> | ||
| 149 | </div> | ||
| 150 | |||
| 151 | <div class="layout_row"> | ||
| 152 | <div class="layout_row_label"><%= t(".template") %></div> | 184 | <div class="layout_row_label"><%= t(".template") %></div> |
| 153 | <div class="layout_row_content"> | 185 | <div class="layout_row_content"> |
| 154 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> | 186 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> |
| @@ -156,13 +188,6 @@ | |||
| 156 | </div> | 188 | </div> |
| 157 | </div> | 189 | </div> |
| 158 | 190 | ||
| 159 | <div class="layout_row"> | ||
| 160 | <div class="layout_row_label"><%= t(".author") %></div> | ||
| 161 | <div class="layout_row_content"> | ||
| 162 | <%= d.select :user_id, user_list, | ||
| 163 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 164 | </div> | ||
| 165 | </div> | ||
| 166 | </div> | 191 | </div> |
| 167 | </details> | 192 | </details> |
| 168 | 193 | ||
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: | |||
| 454 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" | 454 | set_headline_title: "Dieses Foto als Aufmacherbild der Seite verwenden" |
| 455 | remove_image_label: "Bild entfernen" | 455 | remove_image_label: "Bild entfernen" |
| 456 | external_url: "Externe Homepage" | 456 | external_url: "Externe Homepage" |
| 457 | redirect: "Weiterleitung" | ||
| 458 | redirect_enable: "Diese Seite leitet weiter" | ||
| 459 | redirect_hint: "Ohne Ziel-Node wird auf die externe Homepage weitergeleitet." | ||
| 460 | redirect_target_hint: "Bleibt erhalten, wenn die Weiterleitung abgeschaltet wird." | ||
| 461 | redirect_clear: "Ziel entfernen" | ||
| 457 | show: | 462 | show: |
| 458 | status: "Status" | 463 | status: "Status" |
| 459 | head: "Head" | 464 | 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: | |||
| 406 | set_headline_title: "Use this photo as the page's headline image" | 406 | set_headline_title: "Use this photo as the page's headline image" |
| 407 | remove_image_label: "Remove image" | 407 | remove_image_label: "Remove image" |
| 408 | external_url: "External homepage" | 408 | external_url: "External homepage" |
| 409 | redirect: "Redirect" | ||
| 410 | redirect_enable: "This page redirects" | ||
| 411 | redirect_hint: "If the taget node is empty, the external url will be used." | ||
| 412 | redirect_target_hint: "Disabling does not clear the destination" | ||
| 413 | redirect_clear: "Clear redirect" | ||
| 409 | show: | 414 | show: |
| 410 | status: "Status" | 415 | status: "Status" |
| 411 | head: "Head" | 416 | 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 () { | |||
| 63 | search_toggle.addEventListener('click', function (e) { | 63 | search_toggle.addEventListener('click', function (e) { |
| 64 | admin_search.display_toggle(); | 64 | admin_search.display_toggle(); |
| 65 | e.preventDefault(); | 65 | e.preventDefault(); |
| 66 | console.log("Toggle"); | ||
| 67 | }); | 66 | }); |
| 68 | } | 67 | } |
| 69 | 68 | ||
| @@ -124,6 +123,32 @@ $(document).ready(function () { | |||
| 124 | }); | 123 | }); |
| 125 | } | 124 | } |
| 126 | 125 | ||
| 126 | document.querySelectorAll('.field_clear').forEach(function (btn) { | ||
| 127 | btn.addEventListener('click', function () { | ||
| 128 | var wrap = btn.closest('.input_group'); | ||
| 129 | wrap.querySelector('input[type=text]').value = ''; | ||
| 130 | var hidden = wrap.dataset.clears; | ||
| 131 | if (hidden) { document.getElementById(hidden).value = ''; } | ||
| 132 | }); | ||
| 133 | }); | ||
| 134 | |||
| 135 | if (document.getElementById('redirect_search_term')) { | ||
| 136 | redirect_search.initialize_search(); | ||
| 137 | |||
| 138 | var mode = document.getElementById('page_redirect'); | ||
| 139 | var field = document.getElementById('redirect_target_field'); | ||
| 140 | mode.addEventListener('change', function () { | ||
| 141 | field.style.display = mode.checked ? '' : 'none'; | ||
| 142 | }); | ||
| 143 | |||
| 144 | var term = document.getElementById('redirect_search_term'); | ||
| 145 | term.addEventListener('input', function () { | ||
| 146 | if (term.value === '') { | ||
| 147 | document.getElementById('page_redirect_node_id').value = ''; | ||
| 148 | } | ||
| 149 | }); | ||
| 150 | } | ||
| 151 | |||
| 127 | var metadata_details = document.getElementById('metadata_details'); | 152 | var metadata_details = document.getElementById('metadata_details'); |
| 128 | if (metadata_details) { | 153 | if (metadata_details) { |
| 129 | var desktop_mq = window.matchMedia('(min-width: 1016px)'); | 154 | 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 = { | |||
| 263 | } | 263 | } |
| 264 | }; | 264 | }; |
| 265 | 265 | ||
| 266 | redirect_search = { | ||
| 267 | initialize_search : function() { | ||
| 268 | initSearchPicker({ | ||
| 269 | inputSelector: "#redirect_search_term", | ||
| 270 | resultsSelector: "#redirect_search_results", | ||
| 271 | showRestricted: true, | ||
| 272 | onSelect: function(node) { | ||
| 273 | $("#redirect_search_term").val(node.title); | ||
| 274 | $("#page_redirect_node_id").val(node.node_id); | ||
| 275 | } | ||
| 276 | }); | ||
| 277 | } | ||
| 278 | }; | ||
| 279 | |||
| 266 | restore_search = { | 280 | restore_search = { |
| 267 | initialize_search : function() { | 281 | initialize_search : function() { |
| 268 | initSearchPicker({ | 282 | 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 { | |||
| 446 | pointer-events: none; | 446 | pointer-events: none; |
| 447 | } | 447 | } |
| 448 | 448 | ||
| 449 | /* Native (non-button_to) submit buttons -- Create/Save/Publish forms. | 449 | /* Native (non-button_to) submit buttons used for Create/Save/Publish |
| 450 | Lower specificity than form.button_to's own rule, so button_to forms | 450 | forms. */ |
| 451 | are correctly unaffected. If a button_to output ever loses its class | ||
| 452 | wrapper, it will silently fall through to this bordered style instead | ||
| 453 | of rendering as a plain link -- worth knowing, not necessarily fixing. */ | ||
| 454 | input[type="submit"] { | 451 | input[type="submit"] { |
| 455 | -webkit-appearance: none; | 452 | -webkit-appearance: none; |
| 456 | appearance: none; | 453 | appearance: none; |
| @@ -470,6 +467,44 @@ input[type="submit"]:hover { | |||
| 470 | background-color: var(--text); | 467 | background-color: var(--text); |
| 471 | } | 468 | } |
| 472 | 469 | ||
| 470 | .input_group { | ||
| 471 | width: 100%; | ||
| 472 | position: relative; | ||
| 473 | display: inline-block; | ||
| 474 | } | ||
| 475 | |||
| 476 | .input_group .field_search_icon { | ||
| 477 | position: absolute; | ||
| 478 | left: 0.5rem; | ||
| 479 | top: 55%; | ||
| 480 | transform: translateY(-50%); | ||
| 481 | color: var(--text-muted); | ||
| 482 | pointer-events: none; | ||
| 483 | } | ||
| 484 | |||
| 485 | .input_group .field_search_icon svg { | ||
| 486 | width: 1em; | ||
| 487 | height: 1em; | ||
| 488 | } | ||
| 489 | |||
| 490 | .input_group input[type="text"].clearable_input { | ||
| 491 | padding-left: 2rem; | ||
| 492 | padding-right: 1.75rem; | ||
| 493 | } | ||
| 494 | |||
| 495 | .field_clear { | ||
| 496 | position: absolute; | ||
| 497 | right: 0.25rem; | ||
| 498 | top: 50%; | ||
| 499 | transform: translateY(-50%); | ||
| 500 | appearance: none; | ||
| 501 | background: none; | ||
| 502 | border: 0; | ||
| 503 | color: var(--text-muted); | ||
| 504 | cursor: pointer; | ||
| 505 | line-height: 1; | ||
| 506 | } | ||
| 507 | |||
| 473 | /* ============================================================ | 508 | /* ============================================================ |
| 474 | Admin dashboard | 509 | Admin dashboard |
| 475 | ============================================================ */ | 510 | ============================================================ */ |
