From 1fa2f3821497d5529a6753cee84cf5ca679e8bcc Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 5 Jul 2026 21:49:21 +0200 Subject: Fix admin search results colliding with other search widgets layouts/admin.html.erb's top-bar search results div shared id "search_results" with nodes#new/nodes#edit's parent-search widget - since the layout renders on every admin page, whichever widget's JS ran last silently won the shared ID, putting top-bar results into the parent-search box on affected pages. Renamed to "menu_search_results" and updated admin_search.js to match. Also modernizes admin_search.js's event bindings from keyup/keydown/ keypress/paste/cut + .attr("value") to input + .val() throughout (menu_items, parent_search, move_to_search) - the multi-event binding was working around old IE compatibility that .val() + "input" already handles correctly. parent_search's result rendering now matches menu_search's convention (real

/ markup with a .result_path span) instead of a bare in a throwaway wrapper div, so the same CSS rule now correctly applies to both. menu_search's JSON response gains node_path per result, matching what admin_search's own results already provide - not yet consumed by parent_search/move_to_search, which still render click-to-select links rather than navigable ones (correct for their purpose - selecting a value, not leaving the page). Known remaining gap, not fixed here: menu_items, parent_search, and move_to_search still all target the literal id "search_results" between themselves. No live collision today since none of the three currently share a page, but it's the same fragility as the bug above - tracked alongside the existing menu_items/parent_search/move_to_search consolidation backlog item rather than treated as resolved. --- app/views/layouts/admin.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 4536693..a7ce68a 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -49,7 +49,7 @@ Search: <%= text_field_tag :search_term, nil, autocomplete: "off" %> <% end %> -

-- cgit v1.3 From ae29567bea75b28bd3f5655b6bf3e6444d51eb38 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 7 Jul 2026 21:41:36 +0200 Subject: Reorganize admin.css and overhaul button/link/flash visual hierarchy Sorts rules by hierarchy (base -> layout -> navigation -> buttons -> tables -> forms -> components), consolidates three duplicated media query breakpoints into one each, and adds section comments. Bundled with this reorganization, since the file was rewritten wholesale: - form.button_to.state_changing: new tinted-pill variant (blue) for Publish/Restore/Unlock/Revoke, matching .destructive's pattern - form.button_to.destructive: now a tinted pill at rest instead of plain colored text, consistent padding at rest and on hover - Scoped wavy-underline link-visibility fix for #page_editor, table.node_table, table.assets_table, table.events_table, table.user_table, .add_child_links, and the dashboard draft list - #flash decoupled from the page's structural nav-to-content spacing (.admin_content_spacer), which the flash div was silently providing as a side effect whenever it rendered --- app/views/layouts/admin.html.erb | 4 +- public/stylesheets/admin.css | 709 ++++++++++++++++++++++----------------- 2 files changed, 413 insertions(+), 300 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index a7ce68a..340eaf2 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -29,13 +29,15 @@ <%= yield :subnavigation %> -
+
+ <% if flash[:notice].present? || flash[:error].present? %>
<%= flash[:notice] %> <% if flash[:error] %> <%= flash[:error] %> <% end %>
+ <% end %>
<%= yield %>
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 3033798..94248a6 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -1,4 +1,6 @@ -/* Layout */ +/* ============================================================ + Base elements + ============================================================ */ body { font-family: Helvetica, Arial, sans-serif; @@ -15,33 +17,160 @@ a:hover { color: #ff9600; } +/* Scoped link visibility: a light, desaturated squiggly underline + signals "clickable" at rest without the weight of a solid line. + Hover still relies on the global a:hover color change above -- this + only adds an at-rest affordance for plain content links that would + otherwise be indistinguishable from body text. Covers: page editor + body content, node listing tables, the child-creation shortcuts on + nodes#show, and the dashboard draft list's Show/Revisions links. */ +#page_editor a, +table.node_table a, +table.assets_table a, +table.events_table a, +table.user_table a, +.add_child_links a, +div#draft_list table td.actions a { + text-decoration: underline; + -webkit-text-decoration-style: wavy; + text-decoration-style: wavy; + text-decoration-color: #b0b0b0; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + th { text-transform: lowercase; } +td { + padding-top: 10px; + padding-bottom: 5px; + padding-right: 10px; +} +/* table#revisions td only overrides padding-right, so its rows still + depend on this rule for vertical padding. table.node_table td resets + padding-top/bottom to 0 explicitly and does not depend on it. */ + +input[type=text], textarea { + font-size: 1rem; + font-family: Helvetica; + border: 1px solid #989898; +} + +select { + font-size: 1rem; + border: 1px solid #989898; +} + +input[type=password] { + border: 1px solid #989898; +} + +input[type=radio] { + border: 1px solid #989898; +} + +/* ============================================================ + Layout + ============================================================ */ + +@media(min-width:1016px) { + #wrapper { + margin: 0 125px; + } +} +@media(max-width:1015px) { + #wrapper { + margin: 0; + width: 100%; + box-sizing: border-box; + } +} + #metadata, #content { margin-left: 5px; } -@media(min-width:1016px) { -#wrapper { - margin: 0 125px; +.right { text-align: right; } +.clear_left { clear: left; } + +/* ============================================================ + Navigation + ============================================================ */ + +#navigation { + position: relative; + margin-top: 10px; } + +#navigation div { float: left; } + +#navigation a:hover { + color: #ffffff; + background-color: #000000; } -@media(max-width:1015px) { -#wrapper { - margin: 0; - width: 100%; - box-sizing: border-box; + +#navigation a.selected, +#metadata a.selected, +#overview_toggle a.selected { + color: #ffffff; + background-color: #000000; } + +#main_navigation a, +#sub_navigation a, +#metadata a, +#overview_toggle a { + letter-spacing: 1px; + padding-left: 5px; + padding-right: 5px; + padding-top: 1px; + padding-bottom: 1px; + text-transform: lowercase; +} + +#metadata a:hover { + color: #ffffff; + background-color: #ff9600; +} + +/* #sub_navigation covers 13 views' Show/Preview/Revisions/metadata + links (see `grep -rn "content_for :subnavigation" app/views/`), + including nodes#edit -- so #page_editor's own subnav links are + already covered here and are not part of the new scoped-underline + rule above. Proposed for full removal per the redesign notes; the + 13-file list is the confirmation needed before scoping that work. */ +#sub_navigation a { + color: #969696; +} + +#sub_navigation a:hover { + color: #ffffff; + background-color: #ff9600; +} + +#sub_navigation a.selected { + color: #ffffff; + background-color: #ff9600; } +.admin_content_spacer { + clear: both; + margin-bottom: 40px; +} + +/* ============================================================ + Flash / notices + ============================================================ */ #flash { - height: 12px; + height: auto; line-height: 20px; - margin-top: 40px; + padding: 6px 10px; + border-left: 3px solid #969696; + background-color: #f1f1f1; margin-bottom: 20px; margin-left: 5px; } @@ -52,7 +181,6 @@ th { text-transform: lowercase; } - #flash img { height: 12px; width: 12px; @@ -68,7 +196,9 @@ span#flash_error, span.warning { padding-bottom: 1px; } -/* Pagination */ +/* ============================================================ + Pagination + ============================================================ */ div.pagination { padding-top: 5px; @@ -79,43 +209,12 @@ div.pagination span.current, div.pagination a:hover { color: #ff9600; } -/* Styles */ - -.right { - text-align: right; -} - -.clear_left { - clear: left; -} - -#navigation { - position: relative; - margin-top: 10px; -} - -#navigation div { - float: left; -} - -#navigation a:hover { - color: #ffffff; - background-color: #000000; -} - -#main_navigation a, -#sub_navigation a, -#metadata a, -#overview_toggle a { - letter-spacing: 1px; - padding-left: 5px; - padding-right: 5px; - padding-top: 1px; - padding-bottom: 1px; - text-transform: lowercase; -} +/* ============================================================ + Buttons + ============================================================ */ -/* button_to forms: render submit inputs as plain links */ +/* button_to forms: render submit inputs/buttons as plain links, + orange on hover; .destructive variant is red. */ form.button_to { display: inline; margin: 0; @@ -136,31 +235,64 @@ form.button_to button[type="submit"] { text-decoration: none; } -form.button_to input[type="submit"]:hover, -form.button_to button[type="submit"]:hover { - color: #ff9600; +form.button_to.state_changing input[type="submit"], +form.button_to.state_changing button[type="submit"] { + color: #0d47a1; + background-color: #e3f2fd; + border-radius: 2px; + padding: 2px 8px; +} + +form.button_to.state_changing input[type="submit"]:hover, +form.button_to.state_changing button[type="submit"]:hover { + color: #ffffff; + background-color: #1565c0; } form.button_to.destructive input[type="submit"], form.button_to.destructive button[type="submit"] { color: #cc0000; + background-color: #fdecea; + border-radius: 2px; + padding: 2px 8px; } form.button_to.destructive input[type="submit"]:hover, form.button_to.destructive button[type="submit"]:hover { color: #ffffff; background-color: #cc0000; +} + +/* 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. */ +input[type="submit"] { + -webkit-appearance: none; + appearance: none; + background: none; + border: 1px solid #000000; border-radius: 2px; - padding: 0 3px; + padding: 4px 12px; + font: inherit; + font-weight: bold; + color: inherit; + cursor: pointer; + text-decoration: none; } -#admin_wizard { - margin-bottom: 2rem; +input[type="submit"]:hover { + color: #ffffff; + background-color: #000000; } -#page_submit a, -.admin_wizard_button a { - color: white !important; +/* ============================================================ + Wizard buttons + ============================================================ */ + +#admin_wizard { + margin-bottom: 2rem; } #page_submit, @@ -173,32 +305,30 @@ form.button_to.destructive button[type="submit"]:hover { background-color: green; } +#page_submit a, +.admin_wizard_button a { + color: white !important; +} + #page_submit:hover, .admin_wizard_button:hover { background-color: lime !important; } -#sub_navigation a { - color: #969696; -} +/* ============================================================ + Tables + ============================================================ */ -#sub_navigation a:hover, #metadata a:hover { - color: #ffffff; - background-color: #ff9600; -} - -#sub_navigation a.selected { - color: #ffffff; - background-color: #ff9600; -} - -#navigation a.selected, #metadata a.selected, #overview_toggle a.selected { - color: #ffffff; - background-color: #000000; +/* Generic fallback header row. Shadowed by table.node_table's own + tr.header (2rem) and duplicated (same values) by table#revisions's + own tr.header below. Applies as-is to #menu_item_list, + div#draft_list table, and table.user_table, none of which define + their own tr.header. */ +table tr.header { + height: 20px; + text-align: left; } -/* Nodes */ - table.node_table { border-collapse: collapse; } @@ -214,6 +344,9 @@ table.node_table th.title { min-width: 4rem; } +/* th.title's min-width is set to 4rem above and 12rem here -- the + later, equally-specific rule wins, so th.title ends up at 12rem and + the 4rem entry for it above is dead. Likely a copy/paste leftover. */ table.node_table th.editor, table.node_table th.title { min-width: 12rem; @@ -237,6 +370,10 @@ table.node_table .node_id { padding-right: 15px; } +table.node_table .actions { + text-transform: lowercase; +} + table.node_table tr.header:hover { background-color: #ffffff; } @@ -245,20 +382,25 @@ table.node_table tr:hover { background-color: #f1f1f1; } -table.node_table .actions { - text-transform: lowercase; -} - #admin_sitemap_table .node_id:before { content: "• "; } -/* Revisions */ - table#revisions { border-collapse: collapse; } +table#revisions tr { + height: 45px; +} + +/* Redundant with the generic table tr.header rule above (identical + values) -- harmless, kept presumably for explicitness. */ +table#revisions tr.header { + height: 20px; + text-align: left; +} + table#revisions td { padding-right: 25px; } @@ -283,61 +425,91 @@ table#revisions tr:hover { background-color: #f1f1f1; } -table#revisions tr.header { - height: 20px; - text-align: left; +table.user_table td.user_login { + padding-right: 30px; } -table#revisions tr { - height: 45px +/* ============================================================ + Node display (nodes#show / nodes#new) + ============================================================ */ + +@media(min-width:1016px) { + div.node_description { + float: left; + width: 110px; + min-height: 2rem; + text-align: right; + text-transform: lowercase; + vertical-align: top; + } + + div.node_content { + margin-left: 120px; + min-height: 2rem; + display: block; + margin-bottom: 1rem; + } } +@media(max-width:1015px) { + div.node_description { + min-height: 2rem; + width: 30em; + text-transform: lowercase; + vertical-align: top; + } -/* Page Editor */ + div.node_content { + min-height: 2rem; + margin-bottom: 1rem; + } +} -input[type=text], textarea { - font-size: 1rem; - font-family: Helvetica; - border: 1px solid #989898; +.node_content.node_info_group { + border: 1px solid #e8e8e8; + border-radius: 6px; + padding: 0.5rem 0.75rem; } -select { - font-size: 1rem; - border: 1px solid #989898; +.node_info_group_items { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 2rem; } -input[type=text]#tag_list, -input[type=text]#node_staged_slug, -input#move_to_search_term { - padding: 5px; +.node_info_item { + min-width: 140px; } -input[type=password] { - border: 1px solid #989898; +.node_info_label { + display: block; + font-weight: normal; + color: #969696; + font-size: 0.85rem; + text-transform: lowercase; } -input[type="submit"] { - -webkit-appearance: none; - appearance: none; - background: none; - border: 1px solid #000000; - border-radius: 2px; - padding: 4px 12px; - font: inherit; - font-weight: bold; - color: inherit; - cursor: pointer; - text-decoration: none; +.node_info_group ul { + margin: 0; + padding-left: 1.25rem; } -input[type="submit"]:hover { - color: #ffffff; - background-color: #000000; +.node_info_group li { + margin-bottom: 0.25rem; } -input[type=radio] { - border: 1px solid #989898; +.field_hint { + display: block; + margin-top: 2px; + font-size: 0.75rem; + color: #969696; + padding-bottom: 4px; } +/* Identical declaration block to #search_results p span.result_path / + #menu_search_results p span.result_path below -- three independent + copies of the same "small gray helper text" style. */ +/* Layout only -- the at-rest visibility (wavy underline) for these + links comes from the scoped rule in Base elements above. */ .add_child_links { margin-top: 0.5rem; } @@ -346,92 +518,9 @@ input[type=radio] { white-space: nowrap; } -div#login_form input[type=text], div#login_form input[type=password] { - width: 150px; -} - - -@media(min-width:1016px) { -input#tag_list, -input#node_staged_slug, -input#move_to_search_term { - width: 690px; -} - -input[type=text]#page_title { - width: 690px; - font-size: 1rem; - padding-top: 6px; - padding-bottom: 4px; - padding-left: 5px; - padding-right: 5px; - font-weight: bold; -} - -textarea#page_abstract { - width: 690px; - height: 150px; - padding: 5px; -} - -#page_editor textarea#page_body { - width: 700px; - height: 600px; -} - -#page_editor #metadata, #page_editor #content, #admin_overview { - margin-left: -125px; -} - -} - -/* Mobile */ -@media(max-width:1015px) { -input#tag_list, -input#node_staged_slug, -input#move_to_search_term { - box-sizing: border-box; - width: 100%; -} - -#page_editor #content, -#page_editor #metadata { - padding-right: 5px; -} - -#page_editor textarea#page_body { - box-sizing: border-box; - width: 800px; - height: 50rem; -} - -#page_editor textarea#page_abstract { - box-sizing: border-box; - height: 8rem; - width: 100%; -} - -input[type=text]#page_title { - box-sizing: border-box; - width: 100%; - font-size: 2rem; - padding-top: 6px; - padding-bottom: 4px; - padding-left: 5px; - font-weight: bold; -} - -input[type=text], textarea { - font-size: 1.5rem; -} -select { - font-size: 1.5rem; -} - -#metadata ul#image_box { - width: 100% !important; -} -} +/* ============================================================ + Page editor / metadata forms + ============================================================ */ #new_node { margin-left: -118px; @@ -441,44 +530,14 @@ select { width: 350px; } -tr {} - -td { - padding-top: 10px; - padding-bottom: 5px; - padding-right: 10px; -} - -@media(min-width:1016px) { -div.node_description { - float: left; - width: 110px; - min-height: 2rem; - text-align: right; - text-transform: lowercase; - vertical-align: top; -} - -div.node_content { - margin-left: 120px; - min-height: 2rem; - display: block; - margin-bottom: 1rem; -} -} -@media(max-width:1015px) { -div.node_description { - min-height: 2rem; - width: 30em; - text-transform: lowercase; - vertical-align: top; -} - -div.node_content { - min-height: 2rem; - margin-bottom: 1rem; +div#login_form input[type=text], div#login_form input[type=password] { + width: 150px; } +input[type=text]#tag_list, +input[type=text]#node_staged_slug, +input#move_to_search_term { + padding: 5px; } div#page_editor { @@ -486,6 +545,95 @@ div#page_editor { margin-left: 10px; } +@media(min-width:1016px) { + input#tag_list, + input#node_staged_slug, + input#move_to_search_term { + width: 690px; + } + + input[type=text]#page_title { + width: 690px; + font-size: 1rem; + padding-top: 6px; + padding-bottom: 4px; + padding-left: 5px; + padding-right: 5px; + font-weight: bold; + } + + textarea#page_abstract { + width: 690px; + height: 150px; + padding: 5px; + } + + #page_editor textarea#page_body { + width: 700px; + height: 600px; + } + + #page_editor #metadata, #page_editor #content, #admin_overview { + margin-left: -125px; + } +} + +@media(max-width:1015px) { + input#tag_list, + input#node_staged_slug, + input#move_to_search_term { + box-sizing: border-box; + width: 100%; + } + + #page_editor #content, + #page_editor #metadata { + padding-right: 5px; + } + + /* Fixed 800px width will overflow narrow viewports despite + border-box -- every sibling rule in this block uses 100% instead. + Worth checking on an actual phone; looks unfinished rather than + intentional. Not changed here since it wasn't part of what you + asked to fix. */ + #page_editor textarea#page_body { + box-sizing: border-box; + width: 800px; + height: 50rem; + } + + #page_editor textarea#page_abstract { + box-sizing: border-box; + height: 8rem; + width: 100%; + } + + input[type=text]#page_title { + box-sizing: border-box; + width: 100%; + font-size: 2rem; + padding-top: 6px; + padding-bottom: 4px; + padding-left: 5px; + font-weight: bold; + } + + input[type=text], textarea { + font-size: 1.5rem; + } + select { + font-size: 1.5rem; + } + + #metadata ul#image_box { + width: 100% !important; + } +} + +/* ============================================================ + Draft list (dashboard widget) + ============================================================ */ + div#draft_list { border: 1px solid #000000; padding: 5px; @@ -501,66 +649,26 @@ div#draft_list table tr.header:hover { background-color: #ffffff; } +div#draft_list table tr:hover { + background-color: #f1f1f1; +} + div#draft_list table td.actions { text-transform: lowercase; white-space: nowrap; } +/* Layout only -- the at-rest visibility (wavy underline) for these + links comes from the scoped rule in Base elements above. */ div#draft_list table td.actions a { letter-spacing: 1px; padding-left: 5px; padding-right: 5px; } -div#draft_list table tr:hover { - background-color: #f1f1f1; -} - -table tr.header { - height: 20px; - text-align: left; -} - -.field_hint { - display: block; - margin-top: 2px; - font-size: 0.75rem; - color: #969696; - padding-bottom: 4px; -} - -.node_content.node_info_group { - border: 1px solid #e8e8e8; - border-radius: 6px; - padding: 0.5rem 0.75rem; -} - -.node_info_group_items { - display: flex; - flex-wrap: wrap; - gap: 0.5rem 2rem; -} - -.node_info_item { - min-width: 140px; -} - -.node_info_label { - display: block; - font-weight: normal; - color: #969696; - font-size: 0.85rem; - text-transform: lowercase; -} - -.node_info_group ul { - margin: 0; - padding-left: 1.25rem; -} - -.node_info_group li { - margin-bottom: 0.25rem; -} +/* ============================================================ + Search widgets + ============================================================ */ #search_widget { position: absolute; @@ -615,6 +723,10 @@ table tr.header { border-bottom: none; } +/* ============================================================ + Menu items (navigation editor) + ============================================================ */ + #menu_item_list { border-collapse: collapse; padding: 5px 5px 5px 5px; @@ -631,6 +743,10 @@ table tr.header { height: 20px; } +#menu_item_list td.menu_item_title { + width: 200px; +} + #menu_item_list td.menu_sort_handle div { background-color: #989898; height: 26px; @@ -649,14 +765,14 @@ table tr.header { cursor: grab; } -#menu_item_list td.menu_item_title { - width: 200px; -} - -.ui-state-highlight td{ +.ui-state-highlight td { height: 20px; } +/* ============================================================ + Image box / browser + ============================================================ */ + #metadata ul#image_box { box-sizing: border-box; margin: 0; @@ -681,8 +797,3 @@ div#image_browser { div#image_browser ul li { list-style-type: none; } - -table.user_list td.user_login { - padding-right: 30px; -} - -- cgit v1.3 From 9c3217df50d462d6be4399e3e654d591bf704df7 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 9 Jul 2026 15:29:40 +0200 Subject: Fix malformed-HTML fallout across RSS, link rewriting, and flash messaging Editors' TinyMCE output can contain unclosed void elements like
, which is valid HTML5 but invalid XML -- three different places assumed the stricter rule and broke or silently misbehaved on the looser one. The Atom feed's block required real, well-formed XML structure but was handed a raw, unescaped body string; switched to type="html" with CGI.escapeHTML, matching how title/summary already handle the same content. rewrite_links_in_body used libxml's strict XML parser to rewrite internal links to be locale-prefixed, which raised on exactly this class of malformed markup -- silently, since the whole method was wrapped in rescue; nil, meaning the link rewrite (not the save) quietly failed with no error anywhere. Replaced with Nokogiri's lenient HTML parser, which repairs malformed void elements rather than rejecting them; also drops the bare rescue now that the actual failure mode it was guarding against shouldn't occur, and fixes two adjacent bugs found while in this method: a typo'd /sytem/uploads/ regex that could never match, and a missing https:// exclusion alongside the existing http:// one. Also addresses stale flash messaging surfaced while testing the above: update's save confirmation was being clobbered by edit's own "locked and ready" notice on the very next request, since nothing distinguished a fresh lock acquisition from a redirect back after saving. The save confirmation now names the next step (publish from Status) and flags a stale translation if one exists, using Page#outdated_translations?, already present but previously unused by any controller. --- app/controllers/nodes_controller.rb | 18 ++++++++++++++--- app/models/page.rb | 39 ++++++++++++++----------------------- app/views/layouts/admin.html.erb | 7 +++++++ app/views/rss/updates.xml.builder | 4 +--- public/stylesheets/admin.css | 9 +++++++++ 5 files changed, 47 insertions(+), 30 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 72d4a3e..38d42d9 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -65,15 +65,16 @@ class NodesController < ApplicationController end def edit + freshly_locked = @node.lock_owner != current_user @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. " \ "Save to keep them, or use \"Discard changes\" below to go back to the last saved version." + elsif freshly_locked + flash.now[:notice] = "Node locked and ready to edit" end rescue LockedByAnotherUser => e flash[:error] = e.message @@ -85,7 +86,18 @@ class NodesController < ApplicationController @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) @node.save_draft!(current_user) - flash[:notice] = "Draft has been saved: #{Time.now}" + flash[:notice] = "Draft saved. Publish your changes in the Status section once you're done." + flash[:status_path] = node_path(@node) + + if @node.draft.translated_locales.size > 1 + stale_locale = @node.draft.translated_locales.find do |locale| + @node.draft.outdated_translations?(locale: locale) + end + if stale_locale + flash[:stale_locale] = stale_locale + flash[:stale_locale_path] = edit_node_path(@node, locale: stale_locale) + end + end if params[:commit] == "Save + Unlock + Exit" @node.unlock! diff --git a/app/models/page.rb b/app/models/page.rb index fff044e..1a98e08 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -243,31 +243,22 @@ class Page < ApplicationRecord end def rewrite_links_in_body - begin - if self.body - tmp_body = "
#{self.body}
" - xml_string = XML::Parser.string( tmp_body ) - xml_doc = xml_string.parse - links = xml_doc.find("//a[not(starts-with(@href, 'http://'))]") - links = links.reject { |l| l[:href] =~ /system\/uploads/ } - locales = I18n.available_locales.reject {|l| l == :root} - - links.each do |link| - unless locales.include? link[:href].slice(1,2).to_sym - unless link[:href] =~ /sytem\/uploads/ - link[:href] = link[:href].sub(/^\//, "/#{I18n.locale}/") - end - end - end - - tmp_body = xml_doc.to_s.gsub(/(\n\|\<\/div\>\n)/, "") - tmp_body.gsub!("", "") - - self.body = tmp_body + return unless self.body + + doc = Nokogiri::HTML::DocumentFragment.parse(self.body) + locales = I18n.available_locales.reject { |l| l == :root } + + doc.css('a').each do |link| + href = link['href'] + next unless href + next if href.start_with?('http://', 'https://') + next if href =~ /system\/uploads/ + + unless locales.include?(href.slice(1, 2)&.to_sym) + link['href'] = href.sub(/^\//, "/#{I18n.locale}/") end - rescue - nil end - end + self.body = doc.to_html + end end diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 340eaf2..79b1380 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -33,6 +33,13 @@ <% if flash[:notice].present? || flash[:error].present? %>
<%= flash[:notice] %> + <% if flash[:status_path] %> + <%= link_to 'Go to Status', flash[:status_path] %> + <% end %> + <% if flash[:stale_locale_path] %> + The <%= flash[:stale_locale] %> translation may be out of date — + <%= link_to 'review it', flash[:stale_locale_path] %> too. + <% end %> <% if flash[:error] %> <%= flash[:error] %> <% end %> diff --git a/app/views/rss/updates.xml.builder b/app/views/rss/updates.xml.builder index 4b2e2f7..27845c4 100644 --- a/app/views/rss/updates.xml.builder +++ b/app/views/rss/updates.xml.builder @@ -22,9 +22,7 @@ xml.feed(:xmlns => "http://www.w3.org/2005/Atom", "xml:base" => @host) do xml.updated(item.updated_at.xmlschema) xml.published(item.published_at.xmlschema) xml.summary(CGI.escapeHTML(item.abstract.to_s)) - xml.content(:type => "xhtml") do - xml.div(item.body, :xmlns => "http://www.w3.org/1999/xhtml") - end + xml.content(CGI.escapeHTML(item.body.to_s), :type => "html") end end diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index f00a658..3f95b0c 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -193,6 +193,15 @@ input[type=radio] { margin-left: 5px; } +#flash a { + text-decoration: underline; + -webkit-text-decoration-style: wavy; + text-decoration-style: wavy; + text-decoration-color: #b0b0b0; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + #flash span { letter-spacing: 1px; margin-right: 10px; -- cgit v1.3 From edb7f16ea65328c30e5be758a069337ade6a95ba Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 9 Jul 2026 20:13:49 +0200 Subject: Remove subnavigation after the last consumer has gone --- app/views/admin/_menu.html.erb | 2 +- app/views/layouts/admin.html.erb | 3 --- public/stylesheets/admin.css | 21 --------------------- 3 files changed, 1 insertion(+), 25 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index c87c5f7..4302987 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -6,4 +6,4 @@ <%= link_to 'Assets', assets_path, selected?('assets') %> <%= link_to 'Events', events_path, selected?('events') %> <%= link_to 'User', users_path, selected?('users') %> -<%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> >  +<%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 79b1380..fb7d6da 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -25,9 +25,6 @@ -
<% if flash[:notice].present? || flash[:error].present? %> diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index fa36c18..38c9e5a 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -133,7 +133,6 @@ input[type=radio] { } #main_navigation a, -#sub_navigation a, #metadata a, #overview_toggle a { letter-spacing: 1px; @@ -149,26 +148,6 @@ input[type=radio] { background-color: #ff9600; } -/* #sub_navigation covers 13 views' Show/Preview/Revisions/metadata - links (see `grep -rn "content_for :subnavigation" app/views/`), - including nodes#edit -- so #page_editor's own subnav links are - already covered here and are not part of the new scoped-underline - rule above. Proposed for full removal per the redesign notes; the - 13-file list is the confirmation needed before scoping that work. */ -#sub_navigation a { - color: #969696; -} - -#sub_navigation a:hover { - color: #ffffff; - background-color: #ff9600; -} - -#sub_navigation a.selected { - color: #ffffff; - background-color: #ff9600; -} - .admin_content_spacer { clear: both; margin-bottom: 40px; -- cgit v1.3 From 85b20bbc20af850906050c2519dd6dc423126aef Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 11 Jul 2026 01:33:48 +0200 Subject: Make /nodes/parameterize_preview route independent --- app/views/layouts/admin.html.erb | 1 + public/javascripts/admin_search.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index fb7d6da..e714c28 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -16,6 +16,7 @@ diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 5be5d57..0e70845 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -201,7 +201,7 @@ parent_search = { clearTimeout(parent_search.path_timeout); parent_search.path_timeout = setTimeout(function() { - $.get("/nodes/parameterize_preview", { title: title }, function(slug) { + $.get(PARAMETERIZE_PREVIEW_URL, { title: title }, function(slug) { $("#resulting_path").text(window.location.origin + "/" + (prefix ? prefix + "/" : "") + slug); }); }, 300); -- 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/layouts/admin.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 4ec70e5ecf792a56e868538738d6c7f63bb6cf24 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 12 Jul 2026 16:33:12 +0200 Subject: Add a grouped tag+node search endpoint for the upcoming dashboard AdminController#dashboard_search returns tags and nodes as separate, labeled groups (via ActsAsTaggableOn::Tag.named_like and Node.editor_search) rather than the flat per-node list every existing picker returns. initSearchPicker gains a renderResults callback option that, when given, replaces the default per-node rendering entirely -- lets the dashboard render its two labeled groups without a second, parallel picker implementation. resultsHeaderHtml (the "Press Enter to see all results" hint) is now threaded through as a third argument to renderResults, so a picker with custom rendering can still show it -- previously only the default per-node branch ever did. Tags link straight to the existing /admin/nodes/tags/:tag view rather than becoming an in-place filter chip. --- app/controllers/admin_controller.rb | 18 +++++ app/views/layouts/admin.html.erb | 1 + config/routes.rb | 9 +-- public/javascripts/admin_interface.js | 4 ++ public/javascripts/admin_search.js | 109 +++++++++++++++++++++--------- public/stylesheets/admin.css | 10 +++ test/controllers/admin_controller_test.rb | 24 +++++++ 7 files changed, 139 insertions(+), 36 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 3c45c49..8445997 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -49,6 +49,24 @@ class AdminController < ApplicationController end end + def dashboard_search + term = params[:search_term] + + if term.blank? + render json: { tags: [], nodes: [] } + return + end + + render json: { + tags: ActsAsTaggableOn::Tag.named_like(term).limit(5).map { |tag| + { name: tag.name, tag_path: tags_nodes_path(tags: tag.name) } + }, + nodes: Node.editor_search(term).limit(10).map { |node| + { node_id: node.id, title: node.title, unique_name: node.unique_name, node_path: node_path(node) } + } + } + end + def menu_search if params[:search_term] == "Root" @results = [Node.root] diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 079346b..00d7075 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -17,6 +17,7 @@ var ADMIN_SEARCH_URL = "<%= admin_search_path %>"; var ADMIN_MENU_SEARCH_URL = "<%= admin_menu_search_path %>"; var PARAMETERIZE_PREVIEW_URL = "<%= parameterize_preview_nodes_path %>"; + var DASHBOARD_SEARCH_URL = "<%= admin_dashboard_search_path %>"; diff --git a/config/routes.rb b/config/routes.rb index bb34bd8..92301e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -66,10 +66,11 @@ Cccms::Application.routes.draw do end end - match '' => 'admin#index', :as => :admin, :via => :get - match 'search' => 'admin#search', :as => :admin_search, :via => :get - match 'menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get - match 'conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get + match '' => 'admin#index', :as => :admin, :via => :get + match 'search' => 'admin#search', :as => :admin_search, :via => :get + match 'menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get + match 'conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get + match 'dashboard_search' => 'admin#dashboard_search', :as => :admin_dashboard_search, :via => :get end match '/logout' => 'sessions#destroy', :as => :logout, :via => :delete diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 8ca5b5e..3f6a0a9 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -31,6 +31,10 @@ $(document).ready(function () { } }); + if ($("#dashboard_search_term").length != 0) { + dashboard_search.initialize(); + } + if ($("#menu_search_term").length != 0) { menu_items.initialize_search(); } diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index f553334..53bcb5e 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -42,9 +42,10 @@ 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 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 renderResults = options.renderResults; // optional, replaces the default per-node rendering entirely var requestId = 0; var timeout; @@ -68,39 +69,44 @@ function initSearchPicker(options) { url: url, data: "search_term=" + term, dataType: "json", - success: function(nodes) { + success: function(data) { if (thisRequest !== requestId) return; results.empty(); - if (nodes.length && resultsHeaderHtml) { - results.append(resultsHeaderHtml); - } - 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]); + + var found; + if (renderResults) { + found = renderResults(data, results, resultsHeaderHtml); + } else { + if (data.length && resultsHeaderHtml) { + results.append(resultsHeaderHtml); + } + found = false; + for (var i = 0; i < data.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; + })(data[i]); + } } + if (found) { results.slideDown(); } else { @@ -115,6 +121,45 @@ function initSearchPicker(options) { }); } +dashboard_search = { + initialize : function() { + initSearchPicker({ + inputSelector: "#dashboard_search_term", + resultsSelector: "#dashboard_search_results", + url: DASHBOARD_SEARCH_URL, + resultsHeaderHtml: "

Press Enter to see all results ⏎

", + renderResults: function(data, results, resultsHeaderHtml) { + var found = false; + + if ((data.tags.length || data.nodes.length) && resultsHeaderHtml) { + results.append(resultsHeaderHtml); + } + + if (data.tags.length) { + results.append("

Tags

"); + data.tags.forEach(function(tag) { + results.append("

" + tag.name + "

"); + found = true; + }); + } + + if (data.nodes.length) { + results.append("

Pages

"); + data.nodes.forEach(function(node) { + results.append( + "

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

" + ); + found = true; + }); + } + + return found; + } + }); + } +}; + menu_items = { initialize_search : function() { initSearchPicker({ diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index da31535..ade3a62 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -1052,6 +1052,16 @@ div#draft_list table td.actions a { border-bottom: none; } +.search_results p.search_group_label { + margin: 8px 0 2px; + padding: 0; + border-bottom: none; + font-size: 0.75rem; + color: #969696; + text-transform: lowercase; + font-weight: normal; +} + /* ============================================================ Menu items (navigation editor) ============================================================ */ diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 13cc1bb..9beaf58 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -34,4 +34,28 @@ class AdminControllerTest < ActionController::TestCase matches = assigns(:mynodes).select { |n| n.id == node.id } assert_equal 1, matches.length end + + test "dashboard_search returns matching tags and nodes grouped separately" do + node = Node.root.children.create!(:slug => "dashboard_search_test") + node.find_or_create_draft(User.find_by_login("aaron")) + node.draft.update(:title => "Biometrics Workshop") + node.draft.tag_list = "biometrics-workshop" + node.draft.save! + + login_as :quentin + get :dashboard_search, params: { :search_term => "biometr" }, :format => :json + + json = JSON.parse(response.body) + assert json["tags"].any? { |t| t["name"] == "biometrics-workshop" } + assert json["nodes"].any? { |n| n["title"] == "Biometrics Workshop" } + end + + test "dashboard_search returns empty results for a blank term" do + login_as :quentin + get :dashboard_search, params: { :search_term => "" }, :format => :json + + json = JSON.parse(response.body) + assert_equal [], json["tags"] + assert_equal [], json["nodes"] + end end -- cgit v1.3 From 4faf0da3a36bfdcf354b095a2cf5b297ca09af65 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 00:41:36 +0200 Subject: UI cleanup pass: search overlay position, remaining table forms The search overlay was positioned against the whole viewport (left: 50%, transform: translateX(-50%)) because it sat outside wider text nav it used to center under. Moved inside #navigation, which already has its own positioning context, so it now anchors to the icon nav itself instead of the page center. admin/search_results.html.erb was a bare, unstyled
    concatenating title and unique_name into one string by hand. Restyled to match the byline convention already used everywhere else (title_for_node, a separate link_to_path byline with the trailing arrow), and results now link to the node's admin show page rather than straight to edit, matching every other node list in the app instead of being the one exception. menu_items#new and sessions#new (the login page) were the last two views still using the pre-node_content/node_description table-based label/field layout -- found via a grep for the old td.description marker, not guessed at. Converting menu_items#new surfaced a real, separate gap: its input fields have never had a width rule, since the existing 690px rule only applies to a few specifically-named field IDs or anything inside #page_editor, neither of which applies here. Same likely gap probably exists on other simple CRUD forms (users/events/assets/occurrences/pages) -- not fixed here, flagged for its own pass rather than guessed at blind. nodes#new's Create button was pushed to the far right of the page by one stray class (node_content right) no sibling create form carries -- not a family-wide inconsistency, one extra class in one file. --- app/views/admin/search_results.html.erb | 11 +++++---- app/views/layouts/admin.html.erb | 17 +++++++------ app/views/menu_items/new.html.erb | 42 +++++++++++++-------------------- app/views/nodes/new.html.erb | 2 +- app/views/sessions/new.html.erb | 31 +++++++++--------------- public/stylesheets/admin.css | 21 ++++++++++++++--- 6 files changed, 61 insertions(+), 63 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/admin/search_results.html.erb b/app/views/admin/search_results.html.erb index f8aece0..a757197 100644 --- a/app/views/admin/search_results.html.erb +++ b/app/views/admin/search_results.html.erb @@ -1,7 +1,10 @@

    Search Results

    -
      -<%- @results.each do |result| %> -
    • <%= link_to "#{result.title} (#{result.unique_name})", edit_node_path(result) %>
    • -<% end %> +
        + <% @results.each do |result| %> +
      • + <%= link_to title_for_node(result), node_path(result) %> + <%= link_to_path("#{result.unique_name} ↗", result.unique_name) %> +
      • + <% end %>
      diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 00d7075..61bb5c3 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -27,6 +27,14 @@ +
      <% if flash[:notice].present? || flash[:error].present? %> @@ -50,14 +58,5 @@
      - - diff --git a/app/views/menu_items/new.html.erb b/app/views/menu_items/new.html.erb index 223cb8e..8957293 100644 --- a/app/views/menu_items/new.html.erb +++ b/app/views/menu_items/new.html.erb @@ -1,30 +1,20 @@

      Add Menu Item

      <%= form_for @menu_item do |f| %> - - - - - - - - - - - - - - - - - - - - - - -
      Search - <%= text_field_tag :menu_search_term %> - -
      Node Id<%= f.text_field :node_id %>
      Path<%= f.text_field :path %>
      Title<%= f.text_field :title %>
      <%= f.submit 'Create' %>
      +
      Search
      +
      + <%= text_field_tag :menu_search_term %> + +
      + +
      Node Id
      +
      <%= f.text_field :node_id %>
      + +
      Path
      +
      <%= f.text_field :path %>
      + +
      Title
      +
      <%= f.text_field :title %>
      + +
      <%= f.submit 'Create' %>
      <% end %> diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index bb7e078..b63a606 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb @@ -46,7 +46,7 @@
      -
      <%= submit_tag "Create" %>
      +
      <%= submit_tag "Create" %>
      <% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 3c10303..23952b7 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,24 +1,15 @@
      <%= form_tag session_path do -%> - - - - - - - - - - - - - - - - - -
      - <%= flash[:error] %>  -
      Login<%= text_field_tag 'login', @login %>
      Password<%= password_field_tag 'password', nil %>
      <%= submit_tag 'log in' %>
      + <% if flash[:error] %> +
      <%= flash[:error] %>
      + <% end %> + +
      Login
      +
      <%= text_field_tag 'login', @login %>
      + +
      Password
      +
      <%= password_field_tag 'password', nil %>
      + +
      <%= submit_tag 'log in' %>
      <% end -%>
      diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index c1cc456..2044ab0 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -26,6 +26,7 @@ a:hover { nodes#show, and the dashboard draft list's Show/Revisions links. */ #page_editor a, .dashboard_widget a, +.search_results_list a, #sitemap a, #flash a, div.pagination a, @@ -941,6 +942,10 @@ div#page_editor { @media(min-width:1016px) { input#tag_list, + input#menu_search_term, + input#menu_item_node_id, + input#menu_item_path, + input#menu_item_title, input#node_staged_slug, input#move_to_search_term { width: 690px; @@ -1073,15 +1078,25 @@ div#draft_list table td.actions a { padding-right: 5px; } +.search_results_list { + list-style: none; + margin: 0; + padding: 0; +} + +.search_results_list li { + padding: 0.5rem 0; + border-bottom: 1px solid #f1f1f1; +} + /* ============================================================ Search widgets ============================================================ */ #search_widget { position: absolute; - top: 20px; - left: 50%; - transform: translateX(-50%); + top: 2.75rem; + left: 0; width: min(520px, 90vw); border: 1px solid #000000; -webkit-box-shadow: 3px 3px 5px #b1b1b1; -- cgit v1.3 From 6730af4b33b207a7a1880cef2706e67313a7e5cd Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 01:01:31 +0200 Subject: Fix search widget input width; remove dead navigation CSS The input's width was calc(100% - 70px), a number tuned to leave room for a "Search: " label that no longer needs to exist -- replaced by a placeholder instead, which also removes the fragile arithmetic entirely rather than re-tuning it. Also picked up a proper border-radius and padding, matching the dashboard's own search input. The width bug's actual proximate cause was a stray #navigation div { float: left } left over from the old text nav, interfering with the icon nav's own flex layout. Going through that whole block with the same scrutiny turned up four more rules worth removing, not fixing: a:hover (identical specificity, later in the file, silently winning the cascade since the icon nav landed), and #metadata/#overview_toggle in the remaining selectors were dead for two different, confirmed reasons -- #overview_toggle was the old wizard's toggle bar, gone along with it, and #metadata here was the pre-details-view subnav toggle, retired before this session and unrelated to the current, still-live #metadata id on the metadata-details section in nodes#edit (a confusing reuse that's caused real problems before). --- app/views/layouts/admin.html.erb | 2 +- public/stylesheets/admin.css | 35 +++++------------------------------ 2 files changed, 6 insertions(+), 31 deletions(-) (limited to 'app/views/layouts/admin.html.erb') diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 61bb5c3..5ae363f 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -30,7 +30,7 @@