From 5d5dbe32455c08044797fc56d1027e992c84f7f0 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:01:41 -0500 Subject: [PATCH 01/24] Add resource name, visit ID, and props filters to activity events Adds three new search fields to the admin activities events page: - Resource Name: text search on resource_title in event properties JSON - Visit ID: integer filter on visit_id (backend existed, adds UI) - Props: full-text search across the entire properties JSON blob Also adds filter badges to the active filters subheader and preserves new params in quick range links. Co-Authored-By: Claude Opus 4.6 --- .../admin/ahoy_activities_controller.rb | 18 ++++++++++ .../admin/ahoy_activities/index.html.erb | 35 ++++++++++++++++++- .../shared/_active_filters_subheader.html.erb | 15 ++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/ahoy_activities_controller.rb b/app/controllers/admin/ahoy_activities_controller.rb index bbde0a21f..0a912acfd 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -44,6 +44,24 @@ def index scope = scope.where(visit_id: params[:visit_id]) end + # Filter by resource name (resource_title in properties JSON) + if params[:resource_name].present? + term = Ahoy::Event.sanitize_sql_like(params[:resource_name]) + scope = scope.where( + "JSON_UNQUOTE(JSON_EXTRACT(ahoy_events.properties, '$.resource_title')) LIKE ?", + "%#{term}%" + ) + end + + # Filter by props (full-text search across properties JSON) + if params[:props].present? + term = Ahoy::Event.sanitize_sql_like(params[:props]) + scope = scope.where( + "CAST(ahoy_events.properties AS CHAR) LIKE ?", + "%#{term}%" + ) + end + # Audience filter scope = apply_audience_filter(scope) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 40f817eb7..82879855f 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -31,6 +31,39 @@ class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + +
+ + <%= text_field_tag :resource_name, + params[:resource_name], + placeholder: "e.g. My Workshop", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> +
+ + +
+ + <%= number_field_tag :visit_id, + params[:visit_id], + placeholder: "e.g. 42", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> +
+ + +
+ + <%= text_field_tag :props, + params[:props], + placeholder: "Search properties...", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> +
+
@@ -74,7 +74,7 @@ <%= date_field_tag :from, params[:from], class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - onchange: "this.form.requestSubmit()" %> + %> @@ -83,7 +83,7 @@ <%= date_field_tag :to, params[:to], class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - onchange: "this.form.requestSubmit()" %> + %> From c0b693128b544d61ffd44b183fb9d5b1ccdd1632 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:07:11 -0500 Subject: [PATCH 09/24] Fix trailing comma syntax errors in events and visits views Remove trailing commas left after inline oninput/onchange attributes were removed, which caused ActionView::SyntaxErrorInTemplate. Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 18 ++++++------------ .../admin/ahoy_activities/visits.html.erb | 9 +++------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index dfeab830d..7513c7856 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -50,8 +50,7 @@ <%= text_field_tag :name, params[:name], placeholder: "e.g. view.workshop", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> @@ -60,8 +59,7 @@ <%= text_field_tag :resource_name, params[:resource_name], placeholder: "e.g. My Workshop", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> @@ -70,8 +68,7 @@ <%= number_field_tag :visit_id, params[:visit_id], placeholder: "e.g. 42", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> @@ -80,8 +77,7 @@ <%= text_field_tag :props, params[:props], placeholder: "Search properties...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> @@ -103,8 +99,7 @@ <%= date_field_tag :from, params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> @@ -112,8 +107,7 @@ <%= date_field_tag :to, params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 6dd30b144..eba790e54 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -64,8 +64,7 @@ <%= number_field_tag :visit_id, params[:visit_id], placeholder: "e.g. 42", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> @@ -73,8 +72,7 @@ <%= date_field_tag :from, params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> @@ -82,8 +80,7 @@ <%= date_field_tag :to, params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm", - %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> From 5d26c0e4cca8c13aca7803a106aeb2c001c2ddfa Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:11:30 -0500 Subject: [PATCH 10/24] Reorder filter chips and search fields, add dismissible X buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chip order: audience → time period → from/to dates → search filters - Move date fields to left of search form (next to time period) - Add X buttons on all chips to dynamically remove filters - New filter_chip Stimulus controller handles chip dismissal Co-Authored-By: Claude Opus 4.6 --- .../controllers/filter_chip_controller.js | 44 ++++++++++++++ .../admin/ahoy_activities/index.html.erb | 32 +++++----- .../admin/ahoy_activities/visits.html.erb | 32 +++++----- .../shared/_active_filters_subheader.html.erb | 60 ++++++++++++------- 4 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 app/frontend/javascript/controllers/filter_chip_controller.js diff --git a/app/frontend/javascript/controllers/filter_chip_controller.js b/app/frontend/javascript/controllers/filter_chip_controller.js new file mode 100644 index 000000000..ac268f3c6 --- /dev/null +++ b/app/frontend/javascript/controllers/filter_chip_controller.js @@ -0,0 +1,44 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="filter-chip" +// Dismisses filter chips by clearing the matching input in the collection +// form (for search params) or navigating to a modified URL (for header params). +export default class extends Controller { + remove(event) { + event.preventDefault(); + const button = event.currentTarget; + const param = button.dataset.param; + const value = button.dataset.value; + + const collectionForm = document.querySelector( + '[data-controller="collection"]', + ); + + // Search params: clear input in collection form and re-submit + if (collectionForm && param !== "audience" && param !== "time_period") { + const input = collectionForm.querySelector(`[name="${param}"]`); + if (input) { + if (input.tagName === "SELECT") { + input.selectedIndex = 0; + } else { + input.value = ""; + } + collectionForm.requestSubmit(); + return; + } + } + + // Header-level params (audience, time_period): rebuild URL and navigate + const url = new URL(window.location); + if (param === "audience" && value) { + const remaining = url.searchParams + .getAll("audience[]") + .filter((v) => v !== value); + url.searchParams.delete("audience[]"); + remaining.forEach((v) => url.searchParams.append("audience[]", v)); + } else { + url.searchParams.delete(param); + } + window.location = url.toString(); + } +} diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 7513c7856..2e8e3d096 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -44,6 +44,22 @@
+ +
+ + <%= date_field_tag :from, + params[:from], + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> +
+ + +
+ + <%= date_field_tag :to, + params[:to], + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> +
+
@@ -94,22 +110,6 @@ class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- -
- - <%= date_field_tag :from, - params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> -
- - -
- - <%= date_field_tag :to, - params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> -
-
<%= link_to "Clear filters", diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index eba790e54..552c6925c 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -44,6 +44,22 @@
+ +
+ + <%= date_field_tag :from, + params[:from], + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> +
+ + +
+ + <%= date_field_tag :to, + params[:to], + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> +
+
@@ -67,22 +83,6 @@ class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- -
- - <%= date_field_tag :from, - params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> -
- - -
- - <%= date_field_tag :to, - params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> -
-
<%= link_to "Clear filters", diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index 276547668..b8c577c19 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -16,7 +16,7 @@ default_audiences = %w[visitors users] audience_values = Array(params[:audience]).reject(&:blank?).presence || default_audiences - audience_labels = audience_values.filter_map { |v| audience_labels_map[v] } + audience_labels = audience_values.filter_map { |v| [v, audience_labels_map[v]] }.select(&:last) has_search_filters = params[:name].present? || params[:resource_name].present? || params[:visit_id].present? || params[:props].present? || @@ -24,53 +24,71 @@ params[:to].present? %> <% if time_label || audience_labels.any? || has_search_filters %> -
+
Filtering: + <% audience_labels.each do |value, label| %> + + <%= label %> + + + <% end %> <% if time_label %> - + <%= time_label %> + <% end %> - <% audience_labels.each do |label| %> - - <%= label %> + <% if params[:from].present? %> + + From: <%= params[:from] %> + + + <% end %> + <% if params[:to].present? %> + + To: <%= params[:to] %> + <% end %> <% if params[:name].present? %> - + Event: <%= params[:name] %> + <% end %> <% if params[:resource_name].present? %> - + Resource: <%= params[:resource_name] %> + <% end %> <% if params[:visit_id].present? %> - + Visit: <%= params[:visit_id] %> + <% end %> <% if params[:props].present? %> - + Props: <%= params[:props] %> - - <% end %> - <% if params[:from].present? %> - - From: <%= params[:from] %> - - <% end %> - <% if params[:to].present? %> - - To: <%= params[:to] %> + <% end %> <% if params[:user_id].present? %> <% user = User.find_by(id: params[:user_id]) %> <% if user %> - + User: <%= user.full_name %> + <% end %> <% end %> From 8ac081e1f59bdcbf5d8a2a6eb846b802ee3184ef Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:12:46 -0500 Subject: [PATCH 11/24] Move filter chips below search boxes and above count row Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 4 ++-- app/views/admin/ahoy_activities/visits.html.erb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 2e8e3d096..de4dd44ec 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -26,8 +26,6 @@ <%= render "admin/shared/activities_tabs" %> - <%= render "admin/shared/active_filters_subheader" %> - <%= form_with url: admin_activities_events_path, method: :get, @@ -122,6 +120,8 @@ <% end %> + <%= render "admin/shared/active_filters_subheader" %> + <%= render "event_count" %> diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 552c6925c..6621642da 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -26,8 +26,6 @@ <%= render "admin/shared/activities_tabs" %> - <%= render "admin/shared/active_filters_subheader" %> - <%= form_with url: admin_activities_visits_path, method: :get, @@ -95,6 +93,8 @@ <% end %> + <%= render "admin/shared/active_filters_subheader" %> + <%= render "visit_count" %> From 5303bf4b4c6c50eee9b1cc390180e4365e1574d7 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:13:24 -0500 Subject: [PATCH 12/24] Change 'Filtering' label to 'Filters applied' Co-Authored-By: Claude Opus 4.6 --- app/views/admin/shared/_active_filters_subheader.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index b8c577c19..fc6e3357c 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -25,7 +25,7 @@ %> <% if time_label || audience_labels.any? || has_search_filters %>
- Filtering: + Filters applied: <% audience_labels.each do |value, label| %> <%= label %> From 54ed660c8b5d6607f968cbf62b1bc4b17322898c Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:15:01 -0500 Subject: [PATCH 13/24] Add placeholder text to date fields and user dropdown - Date fields: "Start date" / "End date" placeholders - User dropdown: "Select user..." prompt - Add focus ring classes to date fields for consistency Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 8 +++++--- app/views/admin/ahoy_activities/visits.html.erb | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index de4dd44ec..71c3f7a8f 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -47,7 +47,8 @@ <%= date_field_tag :from, params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> + placeholder: "Start date", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -55,7 +56,8 @@ <%= date_field_tag :to, params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> + placeholder: "End date", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -104,7 +106,7 @@ :full_name, params[:user_id] ), - include_blank: "All Users", + include_blank: "Select user...", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 6621642da..66f74e79d 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -47,7 +47,8 @@ <%= date_field_tag :from, params[:from], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> + placeholder: "Start date", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -55,7 +56,8 @@ <%= date_field_tag :to, params[:to], - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm" %> + placeholder: "End date", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -68,7 +70,7 @@ :full_name, params[:user_id] ), - include_blank: "All Users", + include_blank: "Select user...", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
From 51e1b753e84d1f82d97ff425790f63908fc57824 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:21:29 -0500 Subject: [PATCH 14/24] Style select placeholder text as grey when blank option is selected Uses CSS :has() to detect when the empty-value option is checked and applies grey text color, matching text input placeholder styling. Co-Authored-By: Claude Opus 4.6 --- app/frontend/stylesheets/application.tailwind.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index 5032fec7e..4f1d6683b 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -106,6 +106,15 @@ select.select-placeholder { font-size: 0.875rem !important; } +/* Grey out select text when blank/placeholder option is selected */ +select:has(> option[value=""]:checked) { + color: #9ca3af; +} + +select option { + color: #111827; +} + .is-active { @apply text-primary border-b-2 border-primary font-bold bg-gray-100; } From 7214f9a43b89c773aeba2e83dae3b4b6a495c288 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:22:52 -0500 Subject: [PATCH 15/24] Use Tailwind classes for select placeholder styling instead of custom CSS - Remove custom CSS :has() rules - Add text-gray-400/text-gray-900 toggle in collection controller - Conditionally apply text-gray-400 in ERB for initial render Co-Authored-By: Claude Opus 4.6 --- .../javascript/controllers/collection_controller.js | 11 +++++++++++ app/frontend/stylesheets/application.tailwind.css | 8 -------- app/views/admin/ahoy_activities/index.html.erb | 2 +- app/views/admin/ahoy_activities/visits.html.erb | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js index ff15c9868..3f320bb8a 100644 --- a/app/frontend/javascript/controllers/collection_controller.js +++ b/app/frontend/javascript/controllers/collection_controller.js @@ -11,6 +11,10 @@ export default class extends Controller { this.toggleClass(event.target); } + if (type === "select-one" || type === "select-multiple") { + this.styleSelectPlaceholder(event.target); + } + if ( type === "checkbox" || type === "radio" || @@ -63,6 +67,7 @@ export default class extends Controller { }); this.element.querySelectorAll('select').forEach(select => { select.selectedIndex = 0; + this.styleSelectPlaceholder(select); }); this.element.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => { if (input.checked) { @@ -74,6 +79,12 @@ export default class extends Controller { this.submitForm(); } + styleSelectPlaceholder(select) { + const isBlank = !select.value; + select.classList.toggle("text-gray-400", isBlank); + select.classList.toggle("text-gray-900", !isBlank); + } + blurOldResults() { const frame = this.element.closest("turbo-frame"); const scope = frame || document; diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index 4f1d6683b..1c6954216 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -106,14 +106,6 @@ select.select-placeholder { font-size: 0.875rem !important; } -/* Grey out select text when blank/placeholder option is selected */ -select:has(> option[value=""]:checked) { - color: #9ca3af; -} - -select option { - color: #111827; -} .is-active { @apply text-primary border-b-2 border-primary font-bold bg-gray-100; diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 71c3f7a8f..24e8a442f 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -107,7 +107,7 @@ params[:user_id] ), include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 66f74e79d..c57300ff8 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -71,7 +71,7 @@ params[:user_id] ), include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %> From 82de604ecd2b268838c61785ea2a8824ba51d345 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:25:09 -0500 Subject: [PATCH 16/24] Match placeholder grey across all search inputs (text-gray-500) - Change select and date placeholders from text-gray-400 to text-gray-500 to match browser default placeholder color in text inputs - Add text-gray-500 conditional class to date inputs when empty - Generalize stylePlaceholder() in collection controller for both selects and date inputs Co-Authored-By: Claude Opus 4.6 --- .../controllers/collection_controller.js | 15 ++++++++------- app/views/admin/ahoy_activities/index.html.erb | 6 +++--- app/views/admin/ahoy_activities/visits.html.erb | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js index 3f320bb8a..ceea92bae 100644 --- a/app/frontend/javascript/controllers/collection_controller.js +++ b/app/frontend/javascript/controllers/collection_controller.js @@ -11,8 +11,8 @@ export default class extends Controller { this.toggleClass(event.target); } - if (type === "select-one" || type === "select-multiple") { - this.styleSelectPlaceholder(event.target); + if (type === "select-one" || type === "select-multiple" || type === "date") { + this.stylePlaceholder(event.target); } if ( @@ -64,10 +64,11 @@ export default class extends Controller { this.element.querySelectorAll('input[type="text"], input[type="search"], input[type="number"], input[type="date"]').forEach(input => { input.value = ''; + if (input.type === "date") this.stylePlaceholder(input); }); this.element.querySelectorAll('select').forEach(select => { select.selectedIndex = 0; - this.styleSelectPlaceholder(select); + this.stylePlaceholder(select); }); this.element.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => { if (input.checked) { @@ -79,10 +80,10 @@ export default class extends Controller { this.submitForm(); } - styleSelectPlaceholder(select) { - const isBlank = !select.value; - select.classList.toggle("text-gray-400", isBlank); - select.classList.toggle("text-gray-900", !isBlank); + stylePlaceholder(el) { + const isBlank = !el.value; + el.classList.toggle("text-gray-500", isBlank); + el.classList.toggle("text-gray-900", !isBlank); } blurOldResults() { diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 24e8a442f..8fa0604dc 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -48,7 +48,7 @@ <%= date_field_tag :from, params[:from], placeholder: "Start date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-500' : 'text-gray-900'}" %> @@ -57,7 +57,7 @@ <%= date_field_tag :to, params[:to], placeholder: "End date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-500' : 'text-gray-900'}" %> @@ -107,7 +107,7 @@ params[:user_id] ), include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-500' : 'text-gray-900'}" %> diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index c57300ff8..3e793451d 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -48,7 +48,7 @@ <%= date_field_tag :from, params[:from], placeholder: "Start date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-500' : 'text-gray-900'}" %> @@ -57,7 +57,7 @@ <%= date_field_tag :to, params[:to], placeholder: "End date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-500' : 'text-gray-900'}" %> @@ -71,7 +71,7 @@ params[:user_id] ), include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-500' : 'text-gray-900'}" %> From 1d918cd149eb39bf4376969a62ceb630cf8b5964 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:26:08 -0500 Subject: [PATCH 17/24] Grey out date picker calendar icon when input is empty Uses opacity on ::-webkit-calendar-picker-indicator to match the grey placeholder text styling. Co-Authored-By: Claude Opus 4.6 --- app/frontend/stylesheets/application.tailwind.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index 1c6954216..080f5597e 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -106,6 +106,10 @@ select.select-placeholder { font-size: 0.875rem !important; } +/* Grey out date picker calendar icon when input is empty */ +input[type="date"].text-gray-500::-webkit-calendar-picker-indicator { + opacity: 0.4; +} .is-active { @apply text-primary border-b-2 border-primary font-bold bg-gray-100; From 1b0bf8b041a395b876080d0d55132e9ddc4fd18b Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:27:19 -0500 Subject: [PATCH 18/24] Make date selector fields slightly wider (flex-[1.3]) Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 4 ++-- app/views/admin/ahoy_activities/visits.html.erb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 8fa0604dc..5fe6a6bf4 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -43,7 +43,7 @@
-
+
<%= date_field_tag :from, params[:from], @@ -52,7 +52,7 @@
-
+
<%= date_field_tag :to, params[:to], diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 3e793451d..709b0578b 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -43,7 +43,7 @@
-
+
<%= date_field_tag :from, params[:from], @@ -52,7 +52,7 @@
-
+
<%= date_field_tag :to, params[:to], From 14c53fc5fb947a767b7be4163c3c78cca960a0a8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:27:59 -0500 Subject: [PATCH 19/24] Make date picker calendar icon always grey Remove the .text-gray-500 condition so the icon stays grey regardless of whether a date is selected. Co-Authored-By: Claude Opus 4.6 --- app/frontend/stylesheets/application.tailwind.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index 080f5597e..7097ba619 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -106,8 +106,8 @@ select.select-placeholder { font-size: 0.875rem !important; } -/* Grey out date picker calendar icon when input is empty */ -input[type="date"].text-gray-500::-webkit-calendar-picker-indicator { +/* Grey out date picker calendar icon */ +input[type="date"]::-webkit-calendar-picker-indicator { opacity: 0.4; } From 9968ce4a8c67ff2df2c1509cf729cc687a4d688e Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:30:01 -0500 Subject: [PATCH 20/24] Adjust search field widths, rename Resource Name to Resource Title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Date selectors: flex-[1.3] → flex-[1.1] (slightly thinner) - Visit ID: flex-1 → flex-[0.7] (thinner) - Resource Title: flex-1 → flex-[1.2] (wider), renamed from Resource Name - Update table column header and filter chip label to match Co-Authored-By: Claude Opus 4.6 --- .../admin/ahoy_activities/_event_results.html.erb | 2 +- app/views/admin/ahoy_activities/index.html.erb | 12 ++++++------ app/views/admin/ahoy_activities/visits.html.erb | 6 +++--- .../admin/shared/_active_filters_subheader.html.erb | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/admin/ahoy_activities/_event_results.html.erb b/app/views/admin/ahoy_activities/_event_results.html.erb index abc0feeb7..25a35a981 100644 --- a/app/views/admin/ahoy_activities/_event_results.html.erb +++ b/app/views/admin/ahoy_activities/_event_results.html.erb @@ -24,7 +24,7 @@ <%= sort_link.call("time", "Time") %> <%= sort_link.call("name", "Event") %> - Resource + Resource Title <%= sort_link.call("user", "User") %> Visit ID Properties diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 5fe6a6bf4..f663d439b 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -43,7 +43,7 @@
-
+
<%= date_field_tag :from, params[:from], @@ -52,7 +52,7 @@
-
+
<%= date_field_tag :to, params[:to], @@ -69,9 +69,9 @@ class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- -
- + +
+ <%= text_field_tag :resource_name, params[:resource_name], placeholder: "e.g. My Workshop", @@ -79,7 +79,7 @@
-
+
<%= number_field_tag :visit_id, params[:visit_id], diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 709b0578b..88a3cde81 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -43,7 +43,7 @@
-
+
<%= date_field_tag :from, params[:from], @@ -52,7 +52,7 @@
-
+
<%= date_field_tag :to, params[:to], @@ -75,7 +75,7 @@
-
+
<%= number_field_tag :visit_id, params[:visit_id], diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index fc6e3357c..2d59076a3 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -63,7 +63,7 @@ <% end %> <% if params[:resource_name].present? %> - Resource: <%= params[:resource_name] %> + Resource title: <%= params[:resource_name] %> From e228de49839d93fbed250ee51192e6dfe165a539 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:32:50 -0500 Subject: [PATCH 21/24] Add autocomplete=off on individual search inputs Browsers often ignore autocomplete=off on the form tag, especially for fields named 'name'. Adding it to each input prevents autofill. Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 4 ++++ app/views/admin/ahoy_activities/visits.html.erb | 1 + 2 files changed, 5 insertions(+) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index f663d439b..ebe650ef5 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -66,6 +66,7 @@ <%= text_field_tag :name, params[:name], placeholder: "e.g. view.workshop", + autocomplete: "off", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -75,6 +76,7 @@ <%= text_field_tag :resource_name, params[:resource_name], placeholder: "e.g. My Workshop", + autocomplete: "off", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -84,6 +86,7 @@ <%= number_field_tag :visit_id, params[:visit_id], placeholder: "e.g. 42", + autocomplete: "off", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
@@ -93,6 +96,7 @@ <%= text_field_tag :props, params[:props], placeholder: "Search properties...", + autocomplete: "off", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 88a3cde81..d665c6472 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -80,6 +80,7 @@ <%= number_field_tag :visit_id, params[:visit_id], placeholder: "e.g. 42", + autocomplete: "off", class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
From 8048635b08df738e742256dbf1c97bdc0adf28fe Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:35:33 -0500 Subject: [PATCH 22/24] Add data-lpignore to Event Name field to prevent LastPass icon Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index ebe650ef5..bb0bfb861 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -67,6 +67,7 @@ params[:name], placeholder: "e.g. view.workshop", autocomplete: "off", + data: { lpignore: "true" }, class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
From cbcdad5b950e643476aa62ee710a1b697b422f50 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:46:14 -0500 Subject: [PATCH 23/24] Add data-form-type=other and data-lpignore to search forms LastPass ignores data-lpignore on individual inputs when it detects a 'name' field. Adding these attributes at the form level tells LastPass this is not a login/identity form. Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 4 +++- app/views/admin/ahoy_activities/visits.html.erb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index bb0bfb861..48262f083 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -30,7 +30,9 @@ <%= form_with url: admin_activities_events_path, method: :get, data: { controller: "collection", - turbo_frame: "event_results" }, + turbo_frame: "event_results", + "form-type": "other", + lpignore: "true" }, autocomplete: "off", class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index d665c6472..6d2a91e18 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -30,7 +30,9 @@ <%= form_with url: admin_activities_visits_path, method: :get, data: { controller: "collection", - turbo_frame: "visit_results" }, + turbo_frame: "visit_results", + "form-type": "other", + lpignore: "true" }, autocomplete: "off", class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> From f12a04fcd096f9e044dc1239b187b98b46201fc4 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 18:53:33 -0500 Subject: [PATCH 24/24] Improve activities search UI and add cross-linking between events/visits - Rename event_name param, case-insensitive resource title search - Add duration column to visits table, make resource title clickable - Cross-link visit IDs from events and event counts from visits - Add checkbox-select and header-form Stimulus controllers - Standardize audience/time period dropdowns across all activity pages - Improve search field layout, placeholders, responsive wrapping - Add filter chip controller registration and audience default handling Co-Authored-By: Claude Opus 4.6 --- .../admin/ahoy_activities_controller.rb | 16 ++-- .../controllers/checkbox_select_controller.js | 32 ++++++++ .../controllers/filter_chip_controller.js | 25 ++++++- .../controllers/header_form_controller.js | 36 +++++++++ app/frontend/javascript/controllers/index.js | 9 +++ .../ahoy_activities/_event_results.html.erb | 52 +++++++++++-- .../ahoy_activities/_visit_results.html.erb | 22 +++++- .../admin/ahoy_activities/charts.html.erb | 15 ++-- .../admin/ahoy_activities/index.html.erb | 73 +++++++++---------- .../admin/ahoy_activities/visits.html.erb | 35 +++++---- app/views/admin/analytics/index.html.erb | 17 ++--- .../shared/_active_filters_subheader.html.erb | 9 ++- .../admin/shared/_audience_dropdown.html.erb | 14 ++-- 13 files changed, 250 insertions(+), 105 deletions(-) create mode 100644 app/frontend/javascript/controllers/checkbox_select_controller.js create mode 100644 app/frontend/javascript/controllers/header_form_controller.js diff --git a/app/controllers/admin/ahoy_activities_controller.rb b/app/controllers/admin/ahoy_activities_controller.rb index a5449adc7..23c9a5a59 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -40,18 +40,18 @@ def visits base_scope = Ahoy::Visit .includes(:user) .left_joins(:events) - .select("ahoy_visits.*, COUNT(ahoy_events.id) AS events_count") + .select("ahoy_visits.*, COUNT(ahoy_events.id) AS events_count, TIMESTAMPDIFF(MINUTE, ahoy_visits.started_at, MAX(ahoy_events.time)) AS duration_minutes") .group("ahoy_visits.id") filtered = apply_visit_filters(base_scope) - sortable = %w[started_at user events_count] + sortable = %w[started_at user events_count duration] @sort = sortable.include?(params[:sort]) ? params[:sort] : "started_at" @sort_direction = params[:direction] == "asc" ? "asc" : "desc" filtered = apply_visit_sort(filtered, @sort, @sort_direction) @visits = filtered.paginate(page: params[:page], per_page: per_page) - base_count = base_scope.count.size - filtered_count = filtered.count.size + base_count = base_scope.reselect("ahoy_visits.id").count.size + filtered_count = filtered.reselect("ahoy_visits.id").count.size @count_display = filtered_count == base_count ? base_count : "#{filtered_count}/#{base_count}" render :visits_lazy @@ -90,15 +90,15 @@ def apply_event_filters(scope) scope = scope.where(resource_type: params[:resource_type]) if params[:resource_type].present? scope = scope.where(resource_id: params[:resource_id]) if params[:resource_id].present? - if params[:name].present? - term = Ahoy::Event.sanitize_sql_like(params[:name]) + if params[:event_name].present? + term = Ahoy::Event.sanitize_sql_like(params[:event_name]) scope = scope.where("ahoy_events.name LIKE ?", "%#{term}%") end if params[:resource_name].present? term = Ahoy::Event.sanitize_sql_like(params[:resource_name]) scope = scope.where( - "ahoy_events.properties->>'$.resource_title' LIKE ?", + "LOWER(ahoy_events.properties->>'$.resource_title') LIKE LOWER(?)", "%#{term}%" ) end @@ -152,6 +152,8 @@ def apply_visit_sort(scope, column, direction) .reorder(Arel.sql("users.first_name #{direction}, users.last_name #{direction}")) when "events_count" scope.reorder(Arel.sql("events_count #{direction}")) + when "duration" + scope.reorder(Arel.sql("duration_minutes #{direction}")) else scope.reorder(started_at: :desc) end diff --git a/app/frontend/javascript/controllers/checkbox_select_controller.js b/app/frontend/javascript/controllers/checkbox_select_controller.js new file mode 100644 index 000000000..f130a858a --- /dev/null +++ b/app/frontend/javascript/controllers/checkbox_select_controller.js @@ -0,0 +1,32 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="checkbox-select" +// Updates a label from checked checkboxes and optionally submits the parent form. +export default class extends Controller { + static targets = ["label"]; + static values = { + labels: Object, + fieldName: String, + allLabel: { type: String, default: "All" }, + autoSubmit: { type: Boolean, default: true } + }; + + update() { + const name = this.fieldNameValue || "audience[]"; + const checkboxes = this.element.querySelectorAll( + `input[name="${name}"]`, + ); + const checked = [...checkboxes] + .filter((c) => c.checked) + .map((c) => this.labelsValue[c.value]); + const total = Object.keys(this.labelsValue).length; + + this.labelTarget.textContent = + checked.length === total ? this.allLabelValue : checked.join(", "); + + if (this.autoSubmitValue) { + const form = this.element.closest("form"); + if (form) form.requestSubmit(); + } + } +} diff --git a/app/frontend/javascript/controllers/filter_chip_controller.js b/app/frontend/javascript/controllers/filter_chip_controller.js index ac268f3c6..ffdfbd0b5 100644 --- a/app/frontend/javascript/controllers/filter_chip_controller.js +++ b/app/frontend/javascript/controllers/filter_chip_controller.js @@ -30,12 +30,31 @@ export default class extends Controller { // Header-level params (audience, time_period): rebuild URL and navigate const url = new URL(window.location); + + // Merge search form values into the URL so they aren't lost on navigation + if (collectionForm) { + const formData = new FormData(collectionForm); + for (const [key, val] of formData.entries()) { + if (!url.searchParams.has(key) && val !== "") { + url.searchParams.set(key, val); + } + } + } + if (param === "audience" && value) { - const remaining = url.searchParams - .getAll("audience[]") - .filter((v) => v !== value); + const defaults = ["visitors", "users"]; + const current = url.searchParams.has("audience[]") + ? url.searchParams.getAll("audience[]") + : defaults; + const remaining = current.filter((v) => v !== value); url.searchParams.delete("audience[]"); remaining.forEach((v) => url.searchParams.append("audience[]", v)); + } else if (param === "time_period") { + const current = url.searchParams.get("time_period") || "past_month"; + url.searchParams.set( + "time_period", + current === "all_time" ? "past_month" : "all_time", + ); } else { url.searchParams.delete(param); } diff --git a/app/frontend/javascript/controllers/header_form_controller.js b/app/frontend/javascript/controllers/header_form_controller.js new file mode 100644 index 000000000..35c0432a2 --- /dev/null +++ b/app/frontend/javascript/controllers/header_form_controller.js @@ -0,0 +1,36 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="header-form" +// Merges search form (collection controller) values into the header form +// before submitting, so search filters aren't lost when changing audience +// or time period. +export default class extends Controller { + submit() { + const collectionForm = document.querySelector( + '[data-controller="collection"]', + ); + + if (collectionForm) { + // Remove any previously injected hidden fields + this.element + .querySelectorAll("input[data-header-form-injected]") + .forEach((el) => el.remove()); + + const formData = new FormData(collectionForm); + for (const [key, val] of formData.entries()) { + // Skip params already handled by the header form (audience, time_period) + if (key === "audience[]" || key === "time_period") continue; + if (val === "") continue; + + const hidden = document.createElement("input"); + hidden.type = "hidden"; + hidden.name = key; + hidden.value = val; + hidden.setAttribute("data-header-form-injected", "true"); + this.element.appendChild(hidden); + } + } + + this.element.requestSubmit(); + } +} diff --git a/app/frontend/javascript/controllers/index.js b/app/frontend/javascript/controllers/index.js index 1c152e3cb..552df245b 100644 --- a/app/frontend/javascript/controllers/index.js +++ b/app/frontend/javascript/controllers/index.js @@ -9,6 +9,9 @@ application.register("anchor-highlight", AnchorHighlightController) import AssetPickerController from "./asset_picker_controller" application.register("asset-picker", AssetPickerController) +import CheckboxSelectController from "./checkbox_select_controller" +application.register("checkbox-select", CheckboxSelectController) + import AutosaveController from "./autosave_controller" application.register("autosave", AutosaveController) @@ -39,6 +42,12 @@ application.register("dropdown", DropdownController) import FilePreviewController from "./file_preview_controller" application.register("file-preview", FilePreviewController) +import FilterChipController from "./filter_chip_controller" +application.register("filter-chip", FilterChipController) + +import HeaderFormController from "./header_form_controller" +application.register("header-form", HeaderFormController) + import InactiveToggleController from "./inactive_toggle_controller" application.register("inactive-toggle", InactiveToggleController) diff --git a/app/views/admin/ahoy_activities/_event_results.html.erb b/app/views/admin/ahoy_activities/_event_results.html.erb index 25a35a981..3d19d36e6 100644 --- a/app/views/admin/ahoy_activities/_event_results.html.erb +++ b/app/views/admin/ahoy_activities/_event_results.html.erb @@ -1,7 +1,7 @@ <%= turbo_stream.replace("event_count", partial: "event_count") %> <%= turbo_stream.replace("active_filters_subheader", partial: "admin/shared/active_filters_subheader") %> <% - sort_base = params.permit(:name, :resource_name, :visit_id, :props, :user_id, :time_period, :from, :to, :per_page, audience: []).to_h.symbolize_keys + sort_base = params.permit(:event_name, :resource_name, :visit_id, :props, :user_id, :time_period, :from, :to, :per_page, audience: []).to_h.symbolize_keys sort_icon = ->(column) { if @sort == column @sort_direction == "asc" ? "fa-arrow-up" : "fa-arrow-down" @@ -23,7 +23,7 @@ <%= sort_link.call("time", "Time") %> - <%= sort_link.call("name", "Event") %> + <%= sort_link.call("name", "Event name") %> Resource Title <%= sort_link.call("user", "User") %> Visit ID @@ -58,7 +58,10 @@ - <%= event.visit_id %> + <%= link_to event.visit_id, + admin_activities_visits_path(visit_id: event.visit_id, time_period: "all_time", audience: %w[visitors users staff]), + class: "text-indigo-600 hover:underline", + data: { turbo: false } %> @@ -70,25 +73,58 @@ <% props = event.properties.dup display_props = [] + res_type = props["resource_type"] + res_id = props["resource_id"] props.keys.select { |k| k.end_with?("_type") }.each do |type_key| prefix = type_key.chomp("_type") id_key = "#{prefix}_id" if props.key?(id_key) - display_props << [prefix, "#{props[type_key]}.find(#{props[id_key]})"] + display_props << ["#{prefix} & ID", "#{props[type_key]} ##{props[id_key]}"] props.delete(type_key) props.delete(id_key) end end - if props.key?("resource_title") - display_props.unshift(["resource_title", props.delete("resource_title")]) + rt_key = props.keys.find { |k| k.downcase == "resource_title" } + if rt_key + display_props.unshift(["resource_title", props.delete(rt_key)]) end props.each do |key, value| - display_props << [key, value.is_a?(Hash) ? value.to_json : value] + if value.is_a?(Hash) + value.each do |sub_key, sub_val| + label = "#{key}.#{sub_key}" + if sub_val.is_a?(Array) && sub_val.size == 2 + display_props << [label, "#{sub_val[0]} → #{sub_val[1]}"] + else + display_props << [label, sub_val] + end + end + elsif value.is_a?(Array) + display_props << [key, value.join(", ")] + else + display_props << [key, value] + end end %> <% else %> diff --git a/app/views/admin/ahoy_activities/_visit_results.html.erb b/app/views/admin/ahoy_activities/_visit_results.html.erb index b2e4d0232..e1e8163cd 100644 --- a/app/views/admin/ahoy_activities/_visit_results.html.erb +++ b/app/views/admin/ahoy_activities/_visit_results.html.erb @@ -22,11 +22,12 @@ - + + @@ -59,17 +60,30 @@ - + + <% end %> <% else %> - diff --git a/app/views/admin/ahoy_activities/charts.html.erb b/app/views/admin/ahoy_activities/charts.html.erb index 93e13251b..66d67ace4 100644 --- a/app/views/admin/ahoy_activities/charts.html.erb +++ b/app/views/admin/ahoy_activities/charts.html.erb @@ -3,24 +3,19 @@

Activities

<%= form_with url: admin_activities_charts_path, method: :get, local: true, - class: "flex items-center gap-4" do %> + class: "flex items-center gap-4", + data: { controller: "header-form" } do %> <%= render "admin/shared/audience_dropdown" %>
<%= select_tag :time_period, options_for_select( - [ - ['All time', 'all_time'], - ['Past day', 'past_day'], - ['Past week', 'past_week'], - ['Past month', 'past_month'], - ['Past year', 'past_year'] - ], - params[:time_period] || 'past_month' + [["All time", "all_time"], ["Past day", "past_day"], ["Past week", "past_week"], ["Past month", "past_month"], ["Past year", "past_year"]], + params[:time_period] || "past_month" ), class: "px-3 py-2 rounded-md border-gray-300 shadow-sm text-sm text-gray-700", - onchange: "this.form.submit()" %> + data: { action: "header-form#submit" } %>
<% end %>
diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 48262f083..004e2c8ca 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -5,10 +5,8 @@

Activities

<%= form_with url: admin_activities_events_path, method: :get, local: true, - class: "flex items-center gap-4" do %> - <% %i[name resource_name visit_id props user_id from to].each do |key| %> - <%= hidden_field_tag key, params[key] if params[key].present? %> - <% end %> + class: "flex items-center gap-4", + data: { controller: "header-form" } do %> <%= render "admin/shared/audience_dropdown" %>
@@ -19,7 +17,7 @@ params[:time_period] || "past_month" ), class: "px-3 py-2 rounded-md border-gray-300 shadow-sm text-sm text-gray-700", - onchange: "this.form.submit()" %> + data: { action: "header-form#submit" } %>
<% end %>
@@ -42,69 +40,59 @@ <% end %> <%= hidden_field_tag :time_period, params[:time_period] || "past_month" %> -
+
-
+
<%= date_field_tag :from, params[:from], placeholder: "Start date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
-
+
<%= date_field_tag :to, params[:to], placeholder: "End date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
-
+
- <%= text_field_tag :name, - params[:name], - placeholder: "e.g. view.workshop", + <%= text_field_tag :event_name, + params[:event_name], + placeholder: "view.workshop", autocomplete: "off", data: { lpignore: "true" }, - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 placeholder:text-gray-400" %>
-
+
<%= text_field_tag :resource_name, params[:resource_name], - placeholder: "e.g. My Workshop", + placeholder: "My Workshop", autocomplete: "off", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> -
- - -
- - <%= number_field_tag :visit_id, - params[:visit_id], - placeholder: "e.g. 42", - autocomplete: "off", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 placeholder:text-gray-400" %>
-
- +
+ <%= text_field_tag :props, params[:props], - placeholder: "Search properties...", + placeholder: "props...", autocomplete: "off", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 placeholder:text-gray-400" %>
-
+
<%= select_tag :user_id, options_from_collection_for_select( @@ -113,15 +101,26 @@ :full_name, params[:user_id] ), - include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + include_blank: "Select user", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %> +
+ + +
+ + <%= number_field_tag :visit_id, + params[:visit_id], + placeholder: "42", + autocomplete: "off", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 placeholder:text-gray-400" %>
-
+
+ <%= link_to "Clear filters", admin_activities_events_path, - class: "btn btn-utility", + class: "btn btn-utility w-full text-center px-3 py-2", data: { action: "collection#clearAndSubmit" } %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 6d2a91e18..a5e33e133 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -5,10 +5,8 @@

Activities

<%= form_with url: admin_activities_visits_path, method: :get, local: true, - class: "flex items-center gap-4" do %> - <% %i[user_id visit_id from to].each do |key| %> - <%= hidden_field_tag key, params[key] if params[key].present? %> - <% end %> + class: "flex items-center gap-4", + data: { controller: "header-form" } do %> <%= render "admin/shared/audience_dropdown" %>
@@ -19,7 +17,7 @@ params[:time_period] || "past_month" ), class: "px-3 py-2 rounded-md border-gray-300 shadow-sm text-sm text-gray-700", - onchange: "this.form.submit()" %> + data: { action: "header-form#submit" } %>
<% end %>
@@ -42,28 +40,28 @@ <% end %> <%= hidden_field_tag :time_period, params[:time_period] || "past_month" %> -
+
-
+
<%= date_field_tag :from, params[:from], placeholder: "Start date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:from].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
-
+
<%= date_field_tag :to, params[:to], placeholder: "End date", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:to].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
-
+
<%= select_tag :user_id, options_from_collection_for_select( @@ -72,25 +70,26 @@ :full_name, params[:user_id] ), - include_blank: "Select user...", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-500' : 'text-gray-900'}" %> + include_blank: "Select user", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 #{params[:user_id].blank? ? 'text-gray-400' : 'text-gray-900'}" %>
-
+
<%= number_field_tag :visit_id, params[:visit_id], - placeholder: "e.g. 42", + placeholder: "42", autocomplete: "off", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 placeholder:text-gray-400" %>
-
+
+ <%= link_to "Clear filters", admin_activities_visits_path, - class: "btn btn-utility", + class: "btn btn-utility w-full text-center px-3 py-2", data: { action: "collection#clearAndSubmit" } %>
diff --git a/app/views/admin/analytics/index.html.erb b/app/views/admin/analytics/index.html.erb index 7b0ab7921..0dfc6b33a 100644 --- a/app/views/admin/analytics/index.html.erb +++ b/app/views/admin/analytics/index.html.erb @@ -3,22 +3,21 @@

Activities

- <%= form_with url: admin_activities_counts_path, method: :get, local: true, class: "flex items-center gap-4" do |f| %> + <%= form_with url: admin_activities_counts_path, method: :get, local: true, + class: "flex items-center gap-4", + data: { controller: "header-form" } do |f| %> <%= render "admin/shared/audience_dropdown" %>
<%= f.label :time_period, "Time period:", class: "text-sm font-medium text-gray-700" %> <%= f.select :time_period, - options_for_select([ - ['All time', 'all_time'], - ['Past day', 'past_day'], - ['Past week', 'past_week'], - ['Past month', 'past_month'], - ['Past year', 'past_year'] - ], params[:time_period] || 'past_month'), + options_for_select( + [["All time", "all_time"], ["Past day", "past_day"], ["Past week", "past_week"], ["Past month", "past_month"], ["Past year", "past_year"]], + params[:time_period] || "past_month" + ), {}, class: "px-3 py-2 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-700", - onchange: "this.form.submit()" %> + data: { action: "header-form#submit" } %>
<% end %>
diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index 2d59076a3..8d148518f 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -2,6 +2,7 @@ <% time_period = params[:time_period].presence || "past_month" time_label = case time_period + when "all_time" then "All time" when "past_day" then "Past day" when "past_week" then "Past week" when "past_month" then "Past month" @@ -18,7 +19,7 @@ audience_values = Array(params[:audience]).reject(&:blank?).presence || default_audiences audience_labels = audience_values.filter_map { |v| [v, audience_labels_map[v]] }.select(&:last) - has_search_filters = params[:name].present? || params[:resource_name].present? || + has_search_filters = params[:event_name].present? || params[:resource_name].present? || params[:visit_id].present? || params[:props].present? || params[:user_id].present? || params[:from].present? || params[:to].present? @@ -54,10 +55,10 @@ class="ml-0.5 text-indigo-600 hover:text-indigo-900">× <% end %> - <% if params[:name].present? %> + <% if params[:event_name].present? %> - Event: <%= params[:name] %> - <% end %> diff --git a/app/views/admin/shared/_audience_dropdown.html.erb b/app/views/admin/shared/_audience_dropdown.html.erb index 43ce56238..c8edd1ada 100644 --- a/app/views/admin/shared/_audience_dropdown.html.erb +++ b/app/views/admin/shared/_audience_dropdown.html.erb @@ -10,16 +10,20 @@ else selected.filter_map { |v| labels_map[v] }.join(", ") end - dropdown_id = "audience-dropdown-#{SecureRandom.hex(4)}" + dropdown_id = "checkbox-select-#{SecureRandom.hex(4)}" %>
"> - -
+ +
IDVisit ID <%= sort_link.call("started_at", "Started") %> <%= sort_link.call("user", "User") %> IP <%= sort_link.call("events_count", "Events") %><%= sort_link.call("duration", "Duration") %> User Agent
<%= link_to visit.attributes["events_count"], - admin_activities_events_path(visit_id: visit.id) %> + admin_activities_events_path(visit_id: visit.id, time_period: "all_time", audience: %w[visitors users staff]), + class: "text-indigo-600 hover:underline", + data: { turbo: false } %> + + <% dm = visit.attributes["duration_minutes"] %> + <% if dm.nil? %> + + <% elsif dm < 1 %> + < 1 min + <% else %> + <%= dm %> min + <% end %> + <%= visit.user_agent %>
+

No visits found