summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-12 16:33:12 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-12 16:33:12 +0200
commit4ec70e5ecf792a56e868538738d6c7f63bb6cf24 (patch)
tree45364f298ce21a7e47de619b7db79b9b77e4ebc1
parent5803c59192b7fb05840d0b452eb64d9f997f3d8f (diff)
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.
-rw-r--r--app/controllers/admin_controller.rb18
-rw-r--r--app/views/layouts/admin.html.erb1
-rw-r--r--config/routes.rb9
-rw-r--r--public/javascripts/admin_interface.js4
-rw-r--r--public/javascripts/admin_search.js109
-rw-r--r--public/stylesheets/admin.css10
-rw-r--r--test/controllers/admin_controller_test.rb24
7 files changed, 139 insertions, 36 deletions
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
49 end 49 end
50 end 50 end
51 51
52 def dashboard_search
53 term = params[:search_term]
54
55 if term.blank?
56 render json: { tags: [], nodes: [] }
57 return
58 end
59
60 render json: {
61 tags: ActsAsTaggableOn::Tag.named_like(term).limit(5).map { |tag|
62 { name: tag.name, tag_path: tags_nodes_path(tags: tag.name) }
63 },
64 nodes: Node.editor_search(term).limit(10).map { |node|
65 { node_id: node.id, title: node.title, unique_name: node.unique_name, node_path: node_path(node) }
66 }
67 }
68 end
69
52 def menu_search 70 def menu_search
53 if params[:search_term] == "Root" 71 if params[:search_term] == "Root"
54 @results = [Node.root] 72 @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 @@
17 var ADMIN_SEARCH_URL = "<%= admin_search_path %>"; 17 var ADMIN_SEARCH_URL = "<%= admin_search_path %>";
18 var ADMIN_MENU_SEARCH_URL = "<%= admin_menu_search_path %>"; 18 var ADMIN_MENU_SEARCH_URL = "<%= admin_menu_search_path %>";
19 var PARAMETERIZE_PREVIEW_URL = "<%= parameterize_preview_nodes_path %>"; 19 var PARAMETERIZE_PREVIEW_URL = "<%= parameterize_preview_nodes_path %>";
20 var DASHBOARD_SEARCH_URL = "<%= admin_dashboard_search_path %>";
20 </script> 21 </script>
21 </head> 22 </head>
22 23
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
66 end 66 end
67 end 67 end
68 68
69 match '' => 'admin#index', :as => :admin, :via => :get 69 match '' => 'admin#index', :as => :admin, :via => :get
70 match 'search' => 'admin#search', :as => :admin_search, :via => :get 70 match 'search' => 'admin#search', :as => :admin_search, :via => :get
71 match 'menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get 71 match 'menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get
72 match 'conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get 72 match 'conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get
73 match 'dashboard_search' => 'admin#dashboard_search', :as => :admin_dashboard_search, :via => :get
73 end 74 end
74 75
75 match '/logout' => 'sessions#destroy', :as => :logout, :via => :delete 76 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 () {
31 } 31 }
32 }); 32 });
33 33
34 if ($("#dashboard_search_term").length != 0) {
35 dashboard_search.initialize();
36 }
37
34 if ($("#menu_search_term").length != 0) { 38 if ($("#menu_search_term").length != 0) {
35 menu_items.initialize_search(); 39 menu_items.initialize_search();
36 } 40 }
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) {
42 var inputSelector = options.inputSelector; 42 var inputSelector = options.inputSelector;
43 var resultsSelector = options.resultsSelector; 43 var resultsSelector = options.resultsSelector;
44 var url = options.url || ADMIN_MENU_SEARCH_URL; 44 var url = options.url || ADMIN_MENU_SEARCH_URL;
45 var onSelect = options.onSelect; // omit for a real-navigation search like admin_search 45 var onSelect = options.onSelect; // omit for a real-navigation search like admin_search
46 var isActive = options.isActive; // optional guard, checked before every search 46 var isActive = options.isActive; // optional guard, checked before every search
47 var resultsHeaderHtml = options.resultsHeaderHtml; // optional, prepended only when results exist 47 var resultsHeaderHtml = options.resultsHeaderHtml; // optional, prepended only when results exist
48 var renderResults = options.renderResults; // optional, replaces the default per-node rendering entirely
48 var requestId = 0; 49 var requestId = 0;
49 var timeout; 50 var timeout;
50 51
@@ -68,39 +69,44 @@ function initSearchPicker(options) {
68 url: url, 69 url: url,
69 data: "search_term=" + term, 70 data: "search_term=" + term,
70 dataType: "json", 71 dataType: "json",
71 success: function(nodes) { 72 success: function(data) {
72 if (thisRequest !== requestId) return; 73 if (thisRequest !== requestId) return;
73 results.empty(); 74 results.empty();
74 if (nodes.length && resultsHeaderHtml) { 75
75 results.append(resultsHeaderHtml); 76 var found;
76 } 77 if (renderResults) {
77 var found = false; 78 found = renderResults(data, results, resultsHeaderHtml);
78 for (var i = 0; i < nodes.length; i++) { 79 } else {
79 (function(node) { 80 if (data.length && resultsHeaderHtml) {
80 var link; 81 results.append(resultsHeaderHtml);
81 if (onSelect) { 82 }
82 link = $( 83 found = false;
83 "<p><a href='#'>" + node.title + 84 for (var i = 0; i < data.length; i++) {
84 "<span class='result_path'>" + node.unique_name + "</span>" + 85 (function(node) {
85 "</a></p>" 86 var link;
86 ); 87 if (onSelect) {
87 link.find("a").bind("click", function() { 88 link = $(
88 onSelect(node); 89 "<p><a href='#'>" + node.title +
89 results.slideUp(); 90 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
90 results.empty(); 91 );
91 return false; 92 link.find("a").bind("click", function() {
92 }); 93 onSelect(node);
93 } else { 94 results.slideUp();
94 link = $( 95 results.empty();
95 "<p><a href='" + node.node_path + "'>" + node.title + 96 return false;
96 "<span class='result_path'>" + node.unique_name + "</span>" + 97 });
97 "</a></p>" 98 } else {
98 ); 99 link = $(
99 } 100 "<p><a href='" + node.node_path + "'>" + node.title +
100 results.append(link); 101 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
101 found = true; 102 );
102 })(nodes[i]); 103 }
104 results.append(link);
105 found = true;
106 })(data[i]);
107 }
103 } 108 }
109
104 if (found) { 110 if (found) {
105 results.slideDown(); 111 results.slideDown();
106 } else { 112 } else {
@@ -115,6 +121,45 @@ function initSearchPicker(options) {
115 }); 121 });
116} 122}
117 123
124dashboard_search = {
125 initialize : function() {
126 initSearchPicker({
127 inputSelector: "#dashboard_search_term",
128 resultsSelector: "#dashboard_search_results",
129 url: DASHBOARD_SEARCH_URL,
130 resultsHeaderHtml: "<p class='search_more'>Press Enter to see all results ⏎</p>",
131 renderResults: function(data, results, resultsHeaderHtml) {
132 var found = false;
133
134 if ((data.tags.length || data.nodes.length) && resultsHeaderHtml) {
135 results.append(resultsHeaderHtml);
136 }
137
138 if (data.tags.length) {
139 results.append("<p class='search_group_label'>Tags</p>");
140 data.tags.forEach(function(tag) {
141 results.append("<p><a href='" + tag.tag_path + "'>" + tag.name + "</a></p>");
142 found = true;
143 });
144 }
145
146 if (data.nodes.length) {
147 results.append("<p class='search_group_label'>Pages</p>");
148 data.nodes.forEach(function(node) {
149 results.append(
150 "<p><a href='" + node.node_path + "'>" + node.title +
151 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
152 );
153 found = true;
154 });
155 }
156
157 return found;
158 }
159 });
160 }
161};
162
118menu_items = { 163menu_items = {
119 initialize_search : function() { 164 initialize_search : function() {
120 initSearchPicker({ 165 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 {
1052 border-bottom: none; 1052 border-bottom: none;
1053} 1053}
1054 1054
1055.search_results p.search_group_label {
1056 margin: 8px 0 2px;
1057 padding: 0;
1058 border-bottom: none;
1059 font-size: 0.75rem;
1060 color: #969696;
1061 text-transform: lowercase;
1062 font-weight: normal;
1063}
1064
1055/* ============================================================ 1065/* ============================================================
1056 Menu items (navigation editor) 1066 Menu items (navigation editor)
1057 ============================================================ */ 1067 ============================================================ */
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
34 matches = assigns(:mynodes).select { |n| n.id == node.id } 34 matches = assigns(:mynodes).select { |n| n.id == node.id }
35 assert_equal 1, matches.length 35 assert_equal 1, matches.length
36 end 36 end
37
38 test "dashboard_search returns matching tags and nodes grouped separately" do
39 node = Node.root.children.create!(:slug => "dashboard_search_test")
40 node.find_or_create_draft(User.find_by_login("aaron"))
41 node.draft.update(:title => "Biometrics Workshop")
42 node.draft.tag_list = "biometrics-workshop"
43 node.draft.save!
44
45 login_as :quentin
46 get :dashboard_search, params: { :search_term => "biometr" }, :format => :json
47
48 json = JSON.parse(response.body)
49 assert json["tags"].any? { |t| t["name"] == "biometrics-workshop" }
50 assert json["nodes"].any? { |n| n["title"] == "Biometrics Workshop" }
51 end
52
53 test "dashboard_search returns empty results for a blank term" do
54 login_as :quentin
55 get :dashboard_search, params: { :search_term => "" }, :format => :json
56
57 json = JSON.parse(response.body)
58 assert_equal [], json["tags"]
59 assert_equal [], json["nodes"]
60 end
37end 61end