From 5f56fa867f073d8d1aecbfbba4a340ea414cfb0d Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 03:48:28 +0200 Subject: Add loadOnFocus to initSearchPicker; asset search shows recent by default initSearchPicker's input handler has always short-circuited on an empty term -- correct for every existing picker, none of which want results appearing before anything's typed. The asset picker does want exactly that (show the last few uploaded images without requiring a search first), so this adds an opt-in loadOnFocus option instead of changing the shared default: it fires once when the input gains focus with no term yet, reusing the same request/render path a real search uses rather than a separate code path. The AJAX call itself was pulled out into a named runSearch function so both triggers could share it without duplicating the success/render logic. RelatedAssetsController#search now treats a blank term as "show the most recently created, unattached images" (limit 5) rather than returning nothing, matching the new client behavior. A real search term still returns up to 10 name-matched results, unchanged. --- app/controllers/related_assets_controller.rb | 18 ++- public/javascripts/admin_search.js | 133 +++++++++++---------- test/controllers/related_assets_controller_test.rb | 16 ++- 3 files changed, 91 insertions(+), 76 deletions(-) diff --git a/app/controllers/related_assets_controller.rb b/app/controllers/related_assets_controller.rb index 906c02c..ae37d2f 100644 --- a/app/controllers/related_assets_controller.rb +++ b/app/controllers/related_assets_controller.rb @@ -3,17 +3,15 @@ class RelatedAssetsController < ApplicationController before_action :find_node def search - term = params[:q].to_s.strip - if term.blank? - render json: [] - return - end - + term = params[:search_term].to_s.strip attached_ids = @node.editable_page.related_assets.pluck(:asset_id) - results = Asset.images - .where("name ILIKE ?", "%#{term}%") - .where.not(id: attached_ids) - .limit(10) + scope = Asset.images.where.not(id: attached_ids) + + results = if term.present? + scope.where("name ILIKE ?", "%#{term}%").limit(10) + else + scope.order(created_at: :desc).limit(5) + end render json: results.map { |a| { id: a.id, name: a.name, thumb_url: a.upload.url(:thumb) } diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 0c768a3..7604bb7 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -42,18 +42,76 @@ 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 renderResults = options.renderResults; // optional, replaces the default per-node rendering entirely + var onSelect = options.onSelect; + var isActive = options.isActive; + var resultsHeaderHtml = options.resultsHeaderHtml; + var renderResults = options.renderResults; + var loadOnFocus = options.loadOnFocus; // optional, fires an initial search on focus with no term typed yet var requestId = 0; var timeout; + var results = $(resultsSelector); + + function runSearch(term) { + var thisRequest = ++requestId; + $.ajax({ + type: "GET", + url: url, + data: "search_term=" + term, + dataType: "json", + success: function(data) { + if (thisRequest !== requestId) return; + results.empty(); + + 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 { + results.slideUp(); + } + }, + error: function(xhr, status, error) { + console.log("Ajax error:", status, error, xhr.status, xhr.responseText); + } + }); + } $(inputSelector).bind("input", function() { if (isActive && !isActive()) return; var term = $(this).val(); - var results = $(resultsSelector); clearTimeout(timeout); if (!term) { @@ -62,63 +120,16 @@ function initSearchPicker(options) { return; } - timeout = setTimeout(function() { - var thisRequest = ++requestId; - $.ajax({ - type: "GET", - url: url, - data: "search_term=" + term, - dataType: "json", - success: function(data) { - if (thisRequest !== requestId) return; - results.empty(); - - 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 { - results.slideUp(); - } - }, - error: function(xhr, status, error) { - console.log("Ajax error:", status, error, xhr.status, xhr.responseText); - } - }); - }, 250); + timeout = setTimeout(function() { runSearch(term); }, 250); }); + + if (loadOnFocus) { + $(inputSelector).bind("focus", function() { + if (isActive && !isActive()) return; + if ($(this).val()) return; // the input handler above already covers a real term + runSearch(""); + }); + } } dashboard_search = { diff --git a/test/controllers/related_assets_controller_test.rb b/test/controllers/related_assets_controller_test.rb index a3082c2..7f6d487 100644 --- a/test/controllers/related_assets_controller_test.rb +++ b/test/controllers/related_assets_controller_test.rb @@ -11,7 +11,7 @@ class RelatedAssetsControllerTest < ActionController::TestCase Asset.create!(:name => "biometrics-poster", :upload_content_type => "image/png") Asset.create!(:name => "chaostreff-flyer", :upload_content_type => "image/png") - get :search, params: { :node_id => node.id, :q => "biometrics" } + get :search, params: { :node_id => node.id, :search_term => "biometrics" } json = JSON.parse(response.body) names = json.map { |a| a["name"] } @@ -20,13 +20,19 @@ class RelatedAssetsControllerTest < ActionController::TestCase assert_not_includes names, "chaostreff-flyer" end - test "search returns an empty list for a blank term" do + test "search with a blank term returns the most recently created assets" do + Asset.delete_all + RelatedAsset.delete_all login_as :quentin - node = Node.root.children.create!(:slug => "related_assets_blank_search_test") + node = Node.root.children.create!(:slug => "related_assets_recent_test") - get :search, params: { :node_id => node.id, :q => "" } + Asset.create!(:name => "older-photo", :upload_content_type => "image/png", :created_at => 2.days.ago) + Asset.create!(:name => "newer-photo", :upload_content_type => "image/png", :created_at => 1.hour.ago) - assert_equal [], JSON.parse(response.body) + get :search, params: { :node_id => node.id, :search_term => "" } + + json = JSON.parse(response.body) + assert_equal ["newer-photo", "older-photo"], json.map { |a| a["name"] } end test "create attaches an asset to the node's editable page" do -- cgit v1.3