From 4be318cf508f164e3cac1ee3854a0497c8223275 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 7 Jul 2026 13:14:46 +0200 Subject: Add link and lists to tinymce, wash pasted blocks clean of classes --- public/javascripts/admin_interface.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index f68fee8..450438f 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -18,11 +18,12 @@ $(document).ready(function () { license_key: 'gpl', promotion: false, menubar: false, - plugins: 'code', + plugins: 'code link lists visualblocks', toolbar: 'bold italic underline | bullist numlist | link unlink | blocks | code', - extended_valid_elements: 'aggregate[tags|limit|order_by|order_direction|partial|conditions]', + extended_valid_elements: 'aggregate[children|tags|limit|order_by|order_direction|partial|conditions]', relative_urls: false, entity_encoding: 'raw', + valid_classes: { '*': '' }, setup: function(editor) { editor.on('init', function() { cccms.setup_autosave(); -- cgit v1.3 From 3d7ddaf18419c983dd179b50eabe253c3eee8194 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:55:25 +0200 Subject: WIP: dynamic rrule builder --- app/views/events/edit.html.erb | 30 ++++++++++++++++++++++++++++-- public/javascripts/admin_interface.js | 6 +++++- 2 files changed, 33 insertions(+), 3 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb index 5aee501..d37c299 100644 --- a/app/views/events/edit.html.erb +++ b/app/views/events/edit.html.erb @@ -18,8 +18,34 @@ <%= f.datetime_select :end_time %>

- <%= f.label :rrule %>
- <%= f.text_field :rrule %> + <%= f.label :rrule, "Recurrence" %> +

+

+ + +

+
+

+

+ <% %w[MO TU WE TH FR SA SU].each do |code| %> + + <% end %> +

+
+ +

+ + <%= select_tag "rrule_excluded_month", options_for_select((1..12).map { |m| [RruleHumanizer::MONTH_NAMES[:de][m-1], m] }), style: "display: none;" %> +

+ Builds the field below automatically. If your pattern is more complex than this covers, just edit it directly below - these controls won't touch it unless you use them. +
+ <%= f.text_field :rrule, id: "event_rrule" %>

<%= f.label :tag_list, "Tags" %>
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 450438f..cb32d5a 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -50,7 +50,11 @@ $(document).ready(function () { if ($("#move_to_search_term").length != 0) { move_to_search.initialize_search(); } - + + if ($("#rrule_builder").length != 0) { + rrule_builder.initialize(); + } + if ($('#recent_changes_toggle').length != 0) { hide_all(); $('#recent_changes_toggle').attr("class", "selected"); -- cgit v1.3 From a6140897f2fbb0f59a639a931e23751fb93719a2 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:57:32 +0200 Subject: Whitespace fixes --- public/javascripts/admin_interface.js | 72 +++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index cb32d5a..67bfc02 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -12,7 +12,7 @@ function hide_all() { $(document).ready(function () { admin_search.initialize(); - + tinymce.init({ selector: 'textarea.with_editor', license_key: 'gpl', @@ -29,24 +29,24 @@ $(document).ready(function () { cccms.setup_autosave(); }); } - }); + }); if ($("#menu_search_term").length != 0) { menu_items.initialize_search(); } - + if ($("#menu_item_list").length != 0) { menu_item_sorter.initialize(); } - + if ($("#metadata").length != 0) { meta_data.initialize(); } - + if ($("#parent_search_term").length != 0) { parent_search.initialize_search(); } - + if ($("#move_to_search_term").length != 0) { move_to_search.initialize_search(); } @@ -59,14 +59,14 @@ $(document).ready(function () { hide_all(); $('#recent_changes_toggle').attr("class", "selected"); $('#recent_changes_table').show(); - + $('#recent_changes_toggle').bind("click", function(){ hide_all(); $('#recent_changes_toggle').attr("class", "selected"); $('#recent_changes_table').show(); return false; }); - + $('#my_work_toggle').bind("click", function(){ hide_all(); $('#my_work_toggle').attr("class", "selected"); @@ -80,7 +80,7 @@ $(document).ready(function () { $('#my_work_table').show(); return false; }); - + $('#current_drafts_toggle').bind("click", function(){ hide_all(); $('#current_drafts_toggle').attr("class", "selected"); @@ -102,18 +102,18 @@ $(document).ready(function () { return false; }); } - - jQuery.ajaxSetup({ + + jQuery.ajaxSetup({ 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");} }); - + $(document).ajaxSend(function(event, request, settings) { if (typeof(AUTH_TOKEN) == "undefined") return; // settings.data is a serialized string like "foo=bar&baz=boink" (or null) settings.data = settings.data || ""; settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN); }); - + }); @@ -143,50 +143,50 @@ meta_data = { cccms = { setup_autosave : function() { - + var elements = { title : $('#page_title'), abstract : $('#page_abstract'), body : $('#page_body_ifr').contents().find('#tinymce'), } - - var page = { + + var page = { cached_title : elements.title.val(), cached_abstract : elements.abstract.val(), cached_body : elements.body.html(), - + title_has_changed : function() { return (elements.title.val() != this.cached_title) }, - + abstract_has_changed : function() { return (elements.abstract.val() != this.cached_abstract) }, - + body_has_changed : function() { return elements.body.html() != this.cached_body } } - + jQuery.fn.submitWithAjax = function(options) { if (page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { - + page.cached_title = elements.title.val(); page.cached_abstract = elements.abstract.val(); page.cached_body = elements.body.html(); - + $("#flash").append(""); $.post(this.attr("action"), $(this).serialize(), null, "script"); - + } }; - + setInterval('$("#page_editor > form").submitWithAjax()', 7000); } } menu_item_sorter = { - + initialize : function() { $("#menu_item_list").sortable({ axis: 'y', @@ -209,24 +209,24 @@ menu_item_sorter = { } }); }, - + placeholder_helper : function(e,ui) { $(".ui-state-highlight").html(""); } } image_interface = { - + initialize : function() { - + $("#image_browser").hide(); image_interface.initialize_sortable_image_box(); image_interface.connect_browser_and_box(); image_interface.set_droppable_behavior(); - image_interface.bind_image_browser_toggle(); + image_interface.bind_image_browser_toggle(); }, - - + + set_droppable_behavior : function() { $("ul#image_box").droppable({ out : function(event, ui) { @@ -242,7 +242,7 @@ image_interface = { } }); }, - + connect_browser_and_box : function() { $("#image_browser ul li").draggable({ connectToSortable : 'ul#image_box', @@ -250,9 +250,9 @@ image_interface = { revert : 'invalid' }); }, - + initialize_sortable_image_box : function() { - + $("ul#image_box").sortable({ revert : true, update : function(event, ui) { @@ -267,9 +267,9 @@ image_interface = { } }); } - }); + }); }, - + bind_image_browser_toggle : function() { $("#image_browser_toggle").bind("click", function(){ if ($("#image_browser_toggle").attr("class") == "unselected") { -- cgit v1.3 From 971cbc3f0087f288fec9ae0320bb2b22d600df5e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:58:06 +0200 Subject: Link new metadata behaviour and layout in css and javascript --- public/javascripts/admin_interface.js | 20 ++------------------ public/stylesheets/admin.css | 4 ++++ 2 files changed, 6 insertions(+), 18 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 67bfc02..4b1d4a9 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -119,24 +119,8 @@ $(document).ready(function () { meta_data = { initialize : function() { - $("#metadata").hide(); - - $("#button").click(function () { - - $("#metadata").slideToggle(1200); - image_interface.initialize(); - - if ($("#button").attr("class") == "unselected") { - $("#button").attr("class", "selected"); - - } - else { - $("#button").attr("class", "unselected"); - $("#image_browser").hide(); - $("#image_browser_toggle").attr("class", "unselected"); - } - - return false; + document.getElementById("metadata_details").addEventListener("toggle", function() { + if (this.open) image_interface.initialize(); }); } }; diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 94248a6..0c0779f 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -93,6 +93,10 @@ input[type=radio] { margin-left: 5px; } +#metadata_details summary { + margin-bottom: 1em; +} + .right { text-align: right; } .clear_left { clear: left; } -- cgit v1.3 From 618aca78a5574de3d5f5f275c73d33a5cdcb9df8 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:58:36 +0200 Subject: WIP: dynamic rrule builder, javascript side --- public/javascripts/admin_interface.js | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) (limited to 'public/javascripts/admin_interface.js') diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 4b1d4a9..3c480aa 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -271,3 +271,104 @@ image_interface = { } +rrule_builder = { + initialize : function() { + rrule_builder.try_populate_from_rrule($("#event_rrule").val()); + + $("input[name='rrule_freq']").bind("change", function() { + if ($(this).val() === "weekly") { rrule_builder.show_weekly_options(); } + else { rrule_builder.show_monthly_options(); } + rrule_builder.sync(); + }); + $("#rrule_monthly_ordinal").bind("change", function() { + $("#rrule_ordinal_fields").toggle($(this).is(":checked")); + rrule_builder.sync(); + }); + $("#rrule_exclude_month").bind("change", function() { + $("#rrule_excluded_month").toggle($(this).is(":checked")); + rrule_builder.sync(); + }); + $("#rrule_builder input, #rrule_builder select").bind("change", rrule_builder.sync); + }, + + show_weekly_options : function() { + $("#rrule_weekly_options").show(); + $("#rrule_monthly_options").hide(); + }, + show_monthly_options : function() { + $("#rrule_weekly_options").hide(); + $("#rrule_monthly_options").show(); + }, + + sync : function() { + var freq = $("input[name='rrule_freq']:checked").val(); + var parts = []; + + if (freq === "weekly") { + parts.push("FREQ=WEEKLY"); + if ($("#rrule_biweekly").is(":checked")) parts.push("INTERVAL=2"); + var days = []; + $("input[id^='rrule_byday_']:checked").each(function() { + days.push($(this).attr("id").replace("rrule_byday_", "")); + }); + if (days.length > 0) parts.push("BYDAY=" + days.join(",")); + } else { + parts.push("FREQ=MONTHLY"); + if ($("#rrule_monthly_ordinal").is(":checked")) { + parts.push("BYDAY=" + $("#rrule_ordinal").val() + $("#rrule_ordinal_day").val()); + } + } + + if ($("#rrule_exclude_month").is(":checked")) { + var excluded = parseInt($("#rrule_excluded_month").val(), 10); + var months = []; + for (var m = 1; m <= 12; m++) { if (m !== excluded) months.push(m); } + parts.push("BYMONTH=" + months.join(",")); + } + + $("#event_rrule").val(parts.join(";")); + }, + + try_populate_from_rrule : function(rrule) { + if (!rrule) return; + var parts = {}; + rrule.split(";").forEach(function(p) { + var kv = p.split("="); + parts[kv[0]] = kv[1]; + }); + if (parts.COUNT || parts.UNTIL) return; + + if (parts.FREQ === "WEEKLY") { + $("input[name='rrule_freq'][value='weekly']").prop("checked", true); + rrule_builder.show_weekly_options(); + if (parts.INTERVAL === "2") $("#rrule_biweekly").prop("checked", true); + if (parts.BYDAY) { + parts.BYDAY.split(",").forEach(function(code) { + $("#rrule_byday_" + code).prop("checked", true); + }); + } + } else if (parts.FREQ === "MONTHLY") { + $("input[name='rrule_freq'][value='monthly']").prop("checked", true); + rrule_builder.show_monthly_options(); + var match = parts.BYDAY && parts.BYDAY.match(/^(-?\d+)([A-Z]{2})$/); + if (match) { + $("#rrule_monthly_ordinal").prop("checked", true); + $("#rrule_ordinal_fields").show(); + $("#rrule_ordinal").val(match[1]); + $("#rrule_ordinal_day").val(match[2]); + } + } else { + return; + } + + if (parts.BYMONTH) { + var included = parts.BYMONTH.split(",").map(function(s) { return parseInt(s, 10); }); + var missing = []; + for (var m = 1; m <= 12; m++) { if (included.indexOf(m) === -1) missing.push(m); } + if (missing.length === 1) { + $("#rrule_exclude_month").prop("checked", true); + $("#rrule_excluded_month").val(missing[0]).show(); + } + } + } +}; -- cgit v1.3 From 08b4cdb711ffe39153264a5c4024fac95cd8d91a Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 11:58:46 +0200 Subject: Whitespace fixes --- public/javascripts/admin_interface.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 3c480aa..41699a1 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -264,12 +264,11 @@ image_interface = { $("#image_browser_toggle").attr("class", "unselected"); $("#image_browser").hide(); } - + return false; }); } } - rrule_builder = { initialize : function() { -- cgit v1.3 From 43e7743ebff1b1f41bbb5ae39bcc5bed7d8982e3 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 8 Jul 2026 16:24:05 +0200 Subject: Autosave posts to its own endpoint surface lock loss immediately Previously piggybacked on the main form's action and relied on a .js.erb response to update #flash; now posts to the dedicated autosave route with an explicit _method=put override, and reacts to a 423 by stopping the timer and showing a persistent banner rather than letting the next Save discover the conflict on its own. --- app/views/nodes/edit.html.erb | 2 +- public/javascripts/admin_interface.js | 38 ++++++++++++++++++++++++++++++++--- public/stylesheets/admin.css | 5 +++++ 3 files changed, 41 insertions(+), 4 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 4693569..1c6cc3a 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -5,7 +5,7 @@ <% end %>

-<%= form_for(@node) do |f| %> + <%= form_for(@node, html: { data: { autosave_url: autosave_node_path(@node), show_url: node_path(@node) } }) do |f| %> <% if @node.errors.any? %>
    <% @node.errors.full_messages.each do |msg| %>
  • <%= msg %>
  • <% end %>
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 41699a1..4107ce8 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -159,13 +159,45 @@ cccms = { page.cached_abstract = elements.abstract.val(); page.cached_body = elements.body.html(); - $("#flash").append(""); - $.post(this.attr("action"), $(this).serialize(), null, "script"); + var data = this.serializeArray().filter(function(field) { + return field.name !== "_method"; + }); + data.push({ name: "_method", value: "put" }); + + $.ajax({ + type: "POST", + url: this.attr("data-autosave-url"), + data: data, + error: function(xhr) { + if (xhr.status === 423) { + clearInterval(cccms.autosave_timer); + cccms.report_lock_lost($("#page_editor > form").attr("data-show-url")); + } + // any other failure: quietly retried on the next tick + } + }); } }; - setInterval('$("#page_editor > form").submitWithAjax()', 7000); + cccms.autosave_timer = setInterval(function() { + $("#page_editor > form").submitWithAjax(); + }, 7000); + }, + + report_lock_lost : function(show_url) { + var $flash = $("#flash"); + if ($flash.length === 0) { + $flash = $("
", { id: "flash" }).insertAfter(".admin_content_spacer"); + } + + var $banner = $("", { "class": "warning" }).text( + "This page is now locked by someone else — your changes here can no longer be saved. Copy anything you need, then " + ); + $banner.append($("", { href: show_url }).text("return to the published page")); + $banner.append("."); + + $flash.html($banner); } } diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 0c0779f..7ae374c 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -200,6 +200,11 @@ span#flash_error, span.warning { padding-bottom: 1px; } +span.warning a { + color: inherit; + text-decoration: underline; +} + /* ============================================================ Pagination ============================================================ */ -- cgit v1.3 From 83a74c6d01e0be50d5806d8253de9f2ad6d5c143 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 9 Jul 2026 18:55:25 +0200 Subject: Rework events#new with the new layout paradigm --- app/views/events/new.html.erb | 89 +++++++++++++++++++---------------- public/javascripts/admin_interface.js | 4 ++ public/javascripts/admin_search.js | 59 +++++++++++++++++++++++ 3 files changed, 112 insertions(+), 40 deletions(-) (limited to 'public/javascripts/admin_interface.js') diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb index 4e1ef53..b20fe48 100644 --- a/app/views/events/new.html.erb +++ b/app/views/events/new.html.erb @@ -1,47 +1,56 @@

New event

+<%= link_to 'Back', safe_return_to(params[:return_to] || events_path) %> + <%= form_for(@event) do |f| %> <%= form_error_messages(f) %> -

- <%= f.label :start_time %>
- <%= f.datetime_select :start_time %> -

-

- <%= f.label :end_time %>
- <%= f.datetime_select :end_time %> -

-

- <%= f.label :rrule %>
- <%= f.text_field :rrule %> -

-

- <%= f.label :tag_list, "Tags" %>
- <%= f.text_field :tag_list %> -

-

- <%= f.label :allday %>
- <%= f.check_box :allday %> -

-

- <%= f.label :url %>
- <%= f.text_field :url %> -

-

- <%= f.label :latitude %>
- <%= f.text_field :latitude %> -

-

- <%= f.label :longitude %>
- <%= f.text_field :longitude %> -

-

- <%= f.hidden_field :node_id %> - <%= hidden_field_tag :return_to, params[:return_to] %> -

-

- <%= f.submit 'Create' %> -

+
+
+
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. +
+ +
Start time
+
<%= f.datetime_select :start_time %>
+ +
End time
+
<%= f.datetime_select :end_time %>
+ +
Rrule
+
<%= f.text_field :rrule %>
+ +
Title
+
+ <%= f.text_field :title %> + Optional — if left blank, the associated node's title is used. +
+ +
Tags
+
<%= f.text_field :tag_list %>
+ +
Allday
+
<%= f.check_box :allday %>
+ +
Url
+
<%= f.text_field :url %>
+ +
Latitude
+
<%= f.text_field :latitude %>
+ +
Longitude
+
<%= f.text_field :longitude %>
+ +
+
<%= hidden_field_tag :return_to, params[:return_to] %>
+ +
+
<%= f.submit 'Create' %>
+
+
<% end %> -<%= link_to 'Back', safe_return_to(params[:return_to] || events_path) %> diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 4107ce8..8ca5b5e 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -51,6 +51,10 @@ $(document).ready(function () { move_to_search.initialize_search(); } + if ($("#event_node_search_term").length != 0) { + event_search.initialize_search(); + } + if ($("#rrule_builder").length != 0) { rrule_builder.initialize(); } diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 6ef9087..5be5d57 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js @@ -283,3 +283,62 @@ move_to_search = { return barf; } } + +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(); + } + }); + }, + + 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; + } +}; -- 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 'public/javascripts/admin_interface.js') 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 8baac265059b70da0148487458ee4077b15f155e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 04:54:33 +0200 Subject: Asset picker: attach/detach/reorder UI, read-only view, autosave fix Replaces nodes#edit's old Images section -- a hidden panel dumping every image asset in the system unfiltered (#image_browser) plus a raw drag-and-drop box (#image_box) -- with a small search-and-click picker built on the endpoint from the last two commits. Attaching posts immediately and appends the new thumbnail via a cloned