From 2bacf6c19eb919033cdd6d1eeaa4b1b486d2288b Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:43:47 -0500 Subject: [PATCH 01/26] Convert events and visits to dynamic Turbo Frame search Replace full-page-reload filtering with the same Turbo Frame + Stimulus collection controller pattern used by stories. Filters now auto-submit with debounce on text input and immediate submit on select/checkbox changes. Adds sortable columns, skeleton loaders, and filtered/total count display. Incorporates resource name, props, and event name filters from #1348. Co-Authored-By: Claude Opus 4.6 --- .../admin/ahoy_activities_controller.rb | 197 ++++++----- .../ahoy_activities/_event_count.html.erb | 7 + .../ahoy_activities/_event_results.html.erb | 114 ++++++ .../ahoy_activities/_events_skeleton.html.erb | 26 ++ .../ahoy_activities/_visit_count.html.erb | 7 + .../ahoy_activities/_visit_results.html.erb | 84 +++++ .../ahoy_activities/_visits_skeleton.html.erb | 26 ++ .../admin/ahoy_activities/index.html.erb | 333 +++++++----------- .../admin/ahoy_activities/index_lazy.html.erb | 3 + .../admin/ahoy_activities/visits.html.erb | 218 +++++------- .../ahoy_activities/visits_lazy.html.erb | 3 + .../shared/_active_filters_subheader.html.erb | 10 + 12 files changed, 592 insertions(+), 436 deletions(-) create mode 100644 app/views/admin/ahoy_activities/_event_count.html.erb create mode 100644 app/views/admin/ahoy_activities/_event_results.html.erb create mode 100644 app/views/admin/ahoy_activities/_events_skeleton.html.erb create mode 100644 app/views/admin/ahoy_activities/_visit_count.html.erb create mode 100644 app/views/admin/ahoy_activities/_visit_results.html.erb create mode 100644 app/views/admin/ahoy_activities/_visits_skeleton.html.erb create mode 100644 app/views/admin/ahoy_activities/index_lazy.html.erb create mode 100644 app/views/admin/ahoy_activities/visits_lazy.html.erb diff --git a/app/controllers/admin/ahoy_activities_controller.rb b/app/controllers/admin/ahoy_activities_controller.rb index badb6fc8f..4bf3ae9b7 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -5,131 +5,158 @@ class AhoyActivitiesController < ApplicationController def index authorize! :ahoy_activity, to: :index? - @users = params[:user_id].present? ? User.where(id: params[:user_id].to_s.split("--")) : nil + if turbo_frame_request? + per_page = params[:per_page].presence&.to_i || 20 + base_scope = Ahoy::Event.includes(:user, :visit) + filtered = apply_event_filters(base_scope) + + sortable = %w[time name user] + @sort = sortable.include?(params[:sort]) ? params[:sort] : "time" + @sort_direction = params[:direction] == "asc" ? "asc" : "desc" + filtered = apply_event_sort(filtered, @sort, @sort_direction) + + @events = filtered.paginate(page: params[:page], per_page: per_page) + base_count = base_scope.count + filtered_count = filtered.count + @count_display = filtered_count == base_count ? base_count : "#{filtered_count}/#{base_count}" + + render :index_lazy + else + render :index + end + end - page = params[:page].presence&.to_i || 1 - per_page = params[:per_page].presence&.to_i || 20 + def show + authorize! :ahoy_activity, to: :show? + @event = Ahoy::Event.includes(:user, :visit).find(params[:id]) + @resource_path = safe_resource_path(@event.resource_type, @event.resource_id) + end - scope = Ahoy::Event.includes(:user, :visit).order(time: :desc) + def visits + authorize! :ahoy_activity, to: :visits? - # Only real content interactions (not search/filter noise) - if params[:prefixes].present? - prefixes = params[:prefixes].split("--").map(&:strip) + if turbo_frame_request? + per_page = params[:per_page].presence&.to_i || 20 + base_scope = Ahoy::Visit + .includes(:user) + .left_joins(:events) + .select("ahoy_visits.*, COUNT(ahoy_events.id) AS events_count") + .group("ahoy_visits.id") + filtered = apply_visit_filters(base_scope) + + sortable = %w[started_at user events_count] + @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 + @count_display = filtered_count == base_count ? base_count : "#{filtered_count}/#{base_count}" + + render :visits_lazy else - prefixes = nil # %w[ create update destroy auth ] # view browse print download - end - if prefixes.present? - scope = scope.where(prefixes.map { |p| "ahoy_events.name LIKE ?" }.join(" OR "), - *prefixes.map { |p| "#{p}.%" }) + render :visits end + end - # Filter by event name - if params[:event_name].present? - scope = scope.where("ahoy_events.name LIKE ?", "%#{Ahoy::Event.sanitize_sql_like(params[:event_name])}%") - end + def charts + authorize! :ahoy_activity, to: :charts? + @creation_velocity_data = creation_velocity_data + prepare_chart_data + end - # Filter by user (if viewing specific user activity) - scope = scope.where(user: @users) if @users.present? + private - # Time filter + def apply_event_filters(scope) + scope = scope.where(user_id: params[:user_id]) if params[:user_id].present? scope = scope.where(time: time_range) if time_range.present? if params[:from].present? - from_time = Time.zone.parse(params[:from]).beginning_of_day - scope = scope.where("ahoy_events.time >= ?", from_time) + scope = scope.where("ahoy_events.time >= ?", Time.zone.parse(params[:from]).beginning_of_day) end - if params[:to].present? - to_time = Time.zone.parse(params[:to]).end_of_day - scope = scope.where("ahoy_events.time <= ?", to_time) - end - - # Filter by visit - if params[:visit_id].present? - scope = scope.where(visit_id: params[:visit_id]) + scope = scope.where("ahoy_events.time <= ?", Time.zone.parse(params[:to]).end_of_day) 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}%" - ) + if params[:prefixes].present? + prefixes = params[:prefixes].split("--").map(&:strip) + scope = scope.where(prefixes.map { "ahoy_events.name LIKE ?" }.join(" OR "), + *prefixes.map { |p| "#{p}.%" }) end - # Audience filter + scope = scope.where(visit_id: params[:visit_id]) if params[:visit_id].present? scope = apply_audience_filter(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? - # Filter by resource type and ID - if params[:resource_type].present? - scope = scope.where(resource_type: params[:resource_type]) + if params[:name].present? + term = Ahoy::Event.sanitize_sql_like(params[:name]) + scope = scope.where("ahoy_events.name LIKE ?", "%#{term}%") end - if params[:resource_id].present? - scope = scope.where(resource_id: params[:resource_id]) + 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 - @events = scope.paginate(page: page, per_page: per_page) - end + if params[:props].present? + term = Ahoy::Event.sanitize_sql_like(params[:props]) + scope = scope.where("CAST(ahoy_events.properties AS CHAR) LIKE ?", "%#{term}%") + end - def show - authorize! :ahoy_activity, to: :show? - @event = Ahoy::Event.includes(:user, :visit).find(params[:id]) - @resource_path = safe_resource_path(@event.resource_type, @event.resource_id) + scope end - def visits - authorize! :ahoy_activity, to: :visits? - - page = params[:page].presence&.to_i || 1 - per_page = params[:per_page].presence&.to_i || 20 - - scope = Ahoy::Visit - .includes(:user) - .left_joins(:events) - .select("ahoy_visits.*, COUNT(ahoy_events.id) AS events_count") - .group("ahoy_visits.id") - .order(started_at: :desc) - - # Filter by user - if params[:user_id].present? - scope = scope.where(user_id: params[:user_id]) - end - - # Filter by visit - if params[:visit_id].present? - scope = scope.where(id: params[:visit_id]) + def apply_event_sort(scope, column, direction) + dir = direction.to_sym + case column + when "time" + scope.reorder(time: dir) + when "name" + scope.reorder(name: dir) + when "user" + scope.left_joins(:user) + .reorder(Arel.sql("users.first_name #{direction}, users.last_name #{direction}")) + else + scope.reorder(time: :desc) end + end - # Time period filter + def apply_visit_filters(scope) + scope = scope.where(user_id: params[:user_id]) if params[:user_id].present? + scope = scope.where(id: params[:visit_id]) if params[:visit_id].present? scope = scope.where(started_at: time_range) if time_range - - # Audience filter scope = apply_audience_filter(scope) - # Date filtering if params[:from].present? - from_time = Time.zone.parse(params[:from]).beginning_of_day - scope = scope.where("ahoy_visits.started_at >= ?", from_time) + scope = scope.where("ahoy_visits.started_at >= ?", Time.zone.parse(params[:from]).beginning_of_day) end - if params[:to].present? - to_time = Time.zone.parse(params[:to]).end_of_day - scope = scope.where("ahoy_visits.started_at <= ?", to_time) + scope = scope.where("ahoy_visits.started_at <= ?", Time.zone.parse(params[:to]).end_of_day) end - @visits = scope.paginate(page: page, per_page: per_page) + scope end - def charts - authorize! :ahoy_activity, to: :charts? - @creation_velocity_data = creation_velocity_data - prepare_chart_data + def apply_visit_sort(scope, column, direction) + dir = direction.to_sym + case column + when "started_at" + scope.reorder(started_at: dir) + when "user" + scope.left_joins(:user) + .reorder(Arel.sql("users.first_name #{direction}, users.last_name #{direction}")) + when "events_count" + scope.reorder(Arel.sql("events_count #{direction}")) + else + scope.reorder(started_at: :desc) + end end - private - def prepare_chart_data events = scoped_events diff --git a/app/views/admin/ahoy_activities/_event_count.html.erb b/app/views/admin/ahoy_activities/_event_count.html.erb new file mode 100644 index 000000000..4249e72a7 --- /dev/null +++ b/app/views/admin/ahoy_activities/_event_count.html.erb @@ -0,0 +1,7 @@ +
+
+
+

Events (<%= @count_display %>)

+
+
+
diff --git a/app/views/admin/ahoy_activities/_event_results.html.erb b/app/views/admin/ahoy_activities/_event_results.html.erb new file mode 100644 index 000000000..bb62569d6 --- /dev/null +++ b/app/views/admin/ahoy_activities/_event_results.html.erb @@ -0,0 +1,114 @@ +<%= turbo_stream.replace("event_count", partial: "event_count") %> +<% + sort_base = params.permit(: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" + else + "fa-sort" + end + } + sort_link = ->(column, label) { + link_to admin_activities_events_path(sort_base.merge(sort: column, direction: (@sort == column && @sort_direction == "desc") ? "asc" : "desc", page: nil)), + data: { turbo_frame: "event_results" }, + class: "inline-flex items-center gap-1 text-gray-700 hover:text-gray-900" do + concat label + concat content_tag(:i, "", class: "fa-solid #{sort_icon.call(column)} text-xs opacity-70") + end + } +%> +
+ + + + + + + + + + + + + + <% if @events.any? %> + <% @events.each do |event| %> + + + + + + + + + + + + + + <% end %> + <% else %> + + + + <% end %> + +
<%= sort_link.call("time", "Time") %><%= sort_link.call("name", "Event") %>Resource<%= sort_link.call("user", "User") %>Visit IDProperties
+ <%= event.time.in_time_zone.strftime("%Y-%m-%d %H:%M %Z") %> + + <%= event.name %> + + <%= event.properties["resource_title"].presence || "—" %> + + <% if event.user %> + <%= link_to event.user.full_name, + user_path(event.user), + class: "text-indigo-600 hover:underline" %> + <% else %> + Guest + <% end %> + + <%= event.visit_id %> + + <% if event.properties.present? && event.properties.any? %> + <%= link_to admin_activities_event_path(event), + class: "cursor-default text-indigo-600 text-xs font-medium bg-indigo-50 px-2 py-0.5 rounded hover:bg-indigo-100" do %> + <%= event.properties.size %> <%= "prop".pluralize(event.properties.size) %> + <% end %> + <% + props = event.properties.dup + display_props = [] + 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]})"] + props.delete(type_key) + props.delete(id_key) + end + end + if props.key?("resource_title") + display_props.unshift(["resource_title", props.delete("resource_title")]) + end + props.each do |key, value| + display_props << [key, value.is_a?(Hash) ? value.to_json : value] + end + %> + + <% else %> + none + <% end %> +
+

No events found

+
+
+ +<% if @events.any? %> +
+ <%= tailwind_paginate @events %> +
+<% end %> diff --git a/app/views/admin/ahoy_activities/_events_skeleton.html.erb b/app/views/admin/ahoy_activities/_events_skeleton.html.erb new file mode 100644 index 000000000..8aa97aae8 --- /dev/null +++ b/app/views/admin/ahoy_activities/_events_skeleton.html.erb @@ -0,0 +1,26 @@ +
+ + + + + + + + + + + + + <% 5.times do %> + + + + + + + + + <% end %> + +
+
diff --git a/app/views/admin/ahoy_activities/_visit_count.html.erb b/app/views/admin/ahoy_activities/_visit_count.html.erb new file mode 100644 index 000000000..3c658ff6a --- /dev/null +++ b/app/views/admin/ahoy_activities/_visit_count.html.erb @@ -0,0 +1,7 @@ +
+
+
+

Visits (<%= @count_display %>)

+
+
+
diff --git a/app/views/admin/ahoy_activities/_visit_results.html.erb b/app/views/admin/ahoy_activities/_visit_results.html.erb new file mode 100644 index 000000000..cc746bf4a --- /dev/null +++ b/app/views/admin/ahoy_activities/_visit_results.html.erb @@ -0,0 +1,84 @@ +<%= turbo_stream.replace("visit_count", partial: "visit_count") %> +<% + sort_base = params.permit(:user_id, :visit_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" + else + "fa-sort" + end + } + sort_link = ->(column, label) { + link_to admin_activities_visits_path(sort_base.merge(sort: column, direction: (@sort == column && @sort_direction == "desc") ? "asc" : "desc", page: nil)), + data: { turbo_frame: "visit_results" }, + class: "inline-flex items-center gap-1 text-gray-700 hover:text-gray-900" do + concat label + concat content_tag(:i, "", class: "fa-solid #{sort_icon.call(column)} text-xs opacity-70") + end + } +%> +
+ + + + + + + + + + + + + + <% if @visits.any? %> + <% @visits.each do |visit| %> + + + + + + + + + + + + + + <% end %> + <% else %> + + + + <% end %> + +
ID<%= sort_link.call("started_at", "Started") %><%= sort_link.call("user", "User") %>IP<%= sort_link.call("events_count", "Events") %>User Agent
+ <%= visit.id %> + + <%= visit.started_at.in_time_zone.strftime("%Y-%m-%d %H:%M %Z") %> + + <% if visit.user %> + <%= link_to visit.user.full_name, + user_path(visit.user), + class: "text-indigo-600 hover:underline" %> + <% else %> + Guest + <% end %> + + <%= visit.ip %> + + <%= link_to visit.attributes["events_count"], + admin_activities_events_path(visit_id: visit.id) %> + + <%= visit.user_agent %> +
+

No visits found

+
+
+ +<% if @visits.any? %> +
+ <%= tailwind_paginate @visits %> +
+<% end %> diff --git a/app/views/admin/ahoy_activities/_visits_skeleton.html.erb b/app/views/admin/ahoy_activities/_visits_skeleton.html.erb new file mode 100644 index 000000000..d7ee3207c --- /dev/null +++ b/app/views/admin/ahoy_activities/_visits_skeleton.html.erb @@ -0,0 +1,26 @@ +
+ + + + + + + + + + + + + <% 5.times do %> + + + + + + + + + <% end %> + +
+
diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 06ffdf8c1..5ba1e9897 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -1,226 +1,131 @@ <% content_for(:page_bg_class, "admin-only bg-blue-100") %>
- -
-

Activities

+ +
+

Activities

+
+ + <%= render "admin/shared/activities_tabs" %> + + + <%= form_with url: admin_activities_events_path, + method: :get, + data: { controller: "collection", + turbo_frame: "event_results" }, + autocomplete: "off", + class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> + +
+ + +
+ + <%= 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", + oninput: "this.form.requestSubmit()" %> +
+ + +
+ + <%= 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", + oninput: "this.form.requestSubmit()" %> +
+ + +
+ + <%= 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", + oninput: "this.form.requestSubmit()" %> +
+ + +
+ + <%= 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", + oninput: "this.form.requestSubmit()" %> +
-
- <%= @events.count %> events -
+
+ +
+ + +
+ + <%= select_tag :user_id, + options_from_collection_for_select( + User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), + :id, + :full_name, + params[:user_id] + ), + include_blank: "All Users", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- <%= render "admin/shared/activities_tabs" %> - - <%= render "admin/shared/active_filters_subheader" %> - - -
- <%= form_with url: request.path, method: :get, local: true do %> - -
- - -
- - <%= text_field_tag :event_name, - params[:event_name], - placeholder: "e.g. Viewed 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" %> -
- - -
- - <%= select_tag :user_id, - options_from_collection_for_select( - User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), - :id, - :full_name, - params[:user_id] - ), - include_blank: "All Users", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> -
- - -
- <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> -
- - -
- - <%= 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" - ), - 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" %> -
- -
- - -
- Quick: - <% safe_params = params.slice(:event_name, :visit_id, :props, :user_id, :from, :to).to_unsafe_h %> - <%= link_to "24h", - url_for(safe_params.merge(from: 1.day.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "7d", - url_for(safe_params.merge(from: 7.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "30d", - url_for(safe_params.merge(from: 30.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> -
- -
- <%= submit_tag "Filter", - class: "bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition" %> -
- - <% end %> + +
+ <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %>
- -
- - - - - - - - - - - - - - <% @events.each do |event| %> - - - - - - - - - - - - - - <% end %> - -
TimeEventResourceUserVisit IDProperties
- <%= event.time.in_time_zone.strftime("%Y-%m-%d %H:%M %Z") %> - - <%= event.name %> - - <%= event.properties["resource_title"].presence || "—" %> - - <% if event.user %> - <%= link_to event.user.full_name, - user_path(event.user), - class: "text-indigo-600 hover:underline" %> - <% else %> - Guest - <% end %> - - <%= event.visit_id %> - - <% if event.properties.present? && event.properties.any? %> - <%= link_to admin_activities_event_path(event), - class: "cursor-default text-indigo-600 text-xs font-medium bg-indigo-50 px-2 py-0.5 rounded hover:bg-indigo-100" do %> - <%= event.properties.size %> <%= "prop".pluralize(event.properties.size) %> - <% end %> - <% - props = event.properties.dup - display_props = [] - # Combine polymorphic _type/_id pairs - 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]})"] - props.delete(type_key) - props.delete(id_key) - end - end - # Show resource_title first, then remaining props - if props.key?("resource_title") - display_props.unshift(["resource_title", props.delete("resource_title")]) - end - props.each do |key, value| - display_props << [key, value.is_a?(Hash) ? value.to_json : value] - end - %> - - <% else %> - none - <% end %> -
+ +
+ + <%= 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" + ), + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- -
- <%= tailwind_paginate @events %> + +
+ + <%= 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", + admin_activities_events_path, + class: "btn btn-utility", + data: { action: "collection#clearAndSubmit" } %> +
+ +
+ + <% end %> + + + <%= render "event_count" %> + + + <% result_src = admin_activities_events_path + "?" + request.query_string %> + <%= turbo_frame_tag "event_results", src: result_src do %> + <%= render "events_skeleton" %> + <% end %>
diff --git a/app/views/admin/ahoy_activities/index_lazy.html.erb b/app/views/admin/ahoy_activities/index_lazy.html.erb new file mode 100644 index 000000000..b9b607ba3 --- /dev/null +++ b/app/views/admin/ahoy_activities/index_lazy.html.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag "event_results" do %> + <%= render "event_results" %> +<% end %> diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 5c3af90ab..bf3a5e5ce 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -1,153 +1,97 @@ <% content_for(:page_bg_class, "admin-only bg-blue-100") %>
-
-

Activities

+ +
+

Activities

+
+ + <%= render "admin/shared/activities_tabs" %> + + + <%= form_with url: admin_activities_visits_path, + method: :get, + data: { controller: "collection", + turbo_frame: "visit_results" }, + autocomplete: "off", + class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> + +
+ + +
+ + <%= select_tag :user_id, + options_from_collection_for_select( + User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), + :id, + :full_name, + params[:user_id] + ), + include_blank: "All Users", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- <%= render "admin/shared/activities_tabs" %> - - <%= render "admin/shared/active_filters_subheader" %> - - -
- <%= form_with url: request.path, method: :get, local: true do %> - -
- - -
- - <%= select_tag :user_id, - options_from_collection_for_select( - User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), - :id, - :full_name, - params[:user_id] - ), - include_blank: "All Users", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> -
- - -
- <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> -
- - -
- - <%= 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" - ), - 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" %> -
- -
- - -
- Quick: - <% safe_params = params.slice(:user_id, :from, :to).to_unsafe_h %> - <%= link_to "24h", - url_for(safe_params.merge(from: 1.day.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "7d", - url_for(safe_params.merge(from: 7.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "30d", - url_for(safe_params.merge(from: 30.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> -
- -
- <%= submit_tag "Filter", - class: "bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition" %> -
+ +
+ + <%= 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", + oninput: "this.form.requestSubmit()" %> +
- <% end %> + +
+ <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %>
- -
- - - - - - - - - - - + +
+ + <%= 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" + ), + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> +
- - <% @visits.each do |visit| %> - - + +
+ + <%= 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", + admin_activities_visits_path, + class: "btn btn-utility", + data: { action: "collection#clearAndSubmit" } %> +
- + - + <% end %> - - - <% end %> - -
IDStartedUserIPEventsUser Agent
- <%= visit.id %> - - <%= visit.started_at.in_time_zone.strftime("%Y-%m-%d %H:%M %Z") %> - - <% if visit.user %> - <%= link_to visit.user.full_name, - user_path(visit.user), - class: "text-indigo-600 hover:underline" %> - <% else %> - Guest - <% end %> - - <%= visit.ip %> - - <%= link_to visit.attributes["events_count"], - admin_activities_events_path(visit_id: visit.id) %> - - <%= visit.user_agent %> -
-
+ + <%= render "visit_count" %> -
- <%= tailwind_paginate @visits %> -
+ + <% result_src = admin_activities_visits_path + "?" + request.query_string %> + <%= turbo_frame_tag "visit_results", src: result_src do %> + <%= render "visits_skeleton" %> + <% end %>
diff --git a/app/views/admin/ahoy_activities/visits_lazy.html.erb b/app/views/admin/ahoy_activities/visits_lazy.html.erb new file mode 100644 index 000000000..ef168388c --- /dev/null +++ b/app/views/admin/ahoy_activities/visits_lazy.html.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag "visit_results" do %> + <%= render "visit_results" %> +<% end %> diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index 140675308..be98d38a9 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -30,6 +30,16 @@ <%= label %> <% 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] %> From 6dcab64a1e3020addba9c4624754f693ef5b8772 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:48:22 -0500 Subject: [PATCH 02/26] Move audience and time period to page header Match the counts/charts layout by placing audience dropdown and time period select in the top-right header area with their own form. Search filter form carries these values as hidden fields so turbo frame requests preserve them. Co-Authored-By: Claude Opus 4.6 --- .../admin/ahoy_activities/index.html.erb | 46 +++++++++++-------- .../admin/ahoy_activities/visits.html.erb | 42 ++++++++++------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 5ba1e9897..58c1c2de6 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -4,10 +4,30 @@

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 %> + <%= 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" + ), + class: "px-3 py-2 rounded-md border-gray-300 shadow-sm text-sm text-gray-700", + onchange: "this.form.submit()" %> +
+ <% end %>
<%= render "admin/shared/activities_tabs" %> + <%= render "admin/shared/active_filters_subheader" %> + <%= form_with url: admin_activities_events_path, method: :get, @@ -16,6 +36,12 @@ autocomplete: "off", class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> + <% # Carry audience + time_period through the turbo frame form %> + <% Array(params[:audience]).reject(&:blank?).each do |aud| %> + <%= hidden_field_tag "audience[]", aud %> + <% end %> + <%= hidden_field_tag :time_period, params[:time_period] || "past_month" %> +
@@ -58,10 +84,6 @@ oninput: "this.form.requestSubmit()" %>
-
- -
-
@@ -76,22 +98,6 @@ class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
- -
- <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> -
- - -
- - <%= 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" - ), - 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 bf3a5e5ce..3ccfb843a 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -4,10 +4,30 @@

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 %> + <%= 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" + ), + class: "px-3 py-2 rounded-md border-gray-300 shadow-sm text-sm text-gray-700", + onchange: "this.form.submit()" %> +
+ <% end %>
<%= render "admin/shared/activities_tabs" %> + <%= render "admin/shared/active_filters_subheader" %> + <%= form_with url: admin_activities_visits_path, method: :get, @@ -16,6 +36,12 @@ autocomplete: "off", class: "bg-white border border-gray-200 rounded-xl shadow-sm p-6 mb-6" do %> + <% # Carry audience + time_period through the turbo frame form %> + <% Array(params[:audience]).reject(&:blank?).each do |aud| %> + <%= hidden_field_tag "audience[]", aud %> + <% end %> + <%= hidden_field_tag :time_period, params[:time_period] || "past_month" %> +
@@ -42,22 +68,6 @@ oninput: "this.form.requestSubmit()" %>
- -
- <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> -
- - -
- - <%= 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" - ), - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> -
-
From f546080f8eadbe3a16aedc56309019c89647e8ea Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:01:41 -0500 Subject: [PATCH 03/26] 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 | 57 +++++++ .../admin/ahoy_activities/index.html.erb | 141 +++++++++++++++++- .../shared/_active_filters_subheader.html.erb | 5 - 3 files changed, 190 insertions(+), 13 deletions(-) diff --git a/app/controllers/admin/ahoy_activities_controller.rb b/app/controllers/admin/ahoy_activities_controller.rb index 4bf3ae9b7..14be54e3f 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -24,6 +24,63 @@ def index else render :index end + if prefixes.present? + scope = scope.where(prefixes.map { |p| "ahoy_events.name LIKE ?" }.join(" OR "), + *prefixes.map { |p| "#{p}.%" }) + end + + # Filter by user (if viewing specific user activity) + scope = scope.where(user: @users) if @users.present? + + # Time filter + scope = scope.where(time: time_range) if time_range.present? + + if params[:from].present? + from_time = Time.zone.parse(params[:from]).beginning_of_day + scope = scope.where("ahoy_events.time >= ?", from_time) + end + + if params[:to].present? + to_time = Time.zone.parse(params[:to]).end_of_day + scope = scope.where("ahoy_events.time <= ?", to_time) + end + + # Filter by visit + if params[:visit_id].present? + 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) + + # Filter by resource type and ID + if params[:resource_type].present? + scope = scope.where(resource_type: params[:resource_type]) + end + + if params[:resource_id].present? + scope = scope.where(resource_id: params[:resource_id]) + end + + @events = scope.paginate(page: page, per_page: per_page) end def show diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 58c1c2de6..93db64c9d 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -54,14 +54,139 @@ oninput: "this.form.requestSubmit()" %>
- -
- - <%= 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", - oninput: "this.form.requestSubmit()" %> + <%= render "admin/shared/activities_tabs" %> + + <%= render "admin/shared/active_filters_subheader" %> + + +
+ <%= form_with url: request.path, method: :get, local: true do %> + +
+ + +
+ + <%= text_field_tag :name, + params[:name], + placeholder: "e.g. Viewed Workshop", + 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" %> +
+ + +
+ + <%= select_tag :user_id, + options_from_collection_for_select( + User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), + :id, + :full_name, + params[:user_id] + ), + include_blank: "All Users", + class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> +
+ + +
+ <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> +
+ + +
+ + <%= 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" + ), + 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" %> +
+ +
+ + +
+ Quick: + <% safe_params = params.slice(:name, :resource_name, :visit_id, :props, :user_id, :from, :to).to_unsafe_h %> + <%= link_to "24h", + url_for(safe_params.merge(from: 1.day.ago.to_date)), + class: "text-indigo-600 hover:underline" %> + + <%= link_to "7d", + url_for(safe_params.merge(from: 7.days.ago.to_date)), + class: "text-indigo-600 hover:underline" %> + + <%= link_to "30d", + url_for(safe_params.merge(from: 30.days.ago.to_date)), + class: "text-indigo-600 hover:underline" %> +
+ +
+ <%= submit_tag "Filter", + class: "bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition" %> +
+ + <% end %>
diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index be98d38a9..37aa33a0d 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -30,11 +30,6 @@ <%= label %> <% end %> - <% if params[:name].present? %> - - Event: <%= params[:name] %> - - <% end %> <% if params[:resource_name].present? %> Resource: <%= params[:resource_name] %> From 2edef2b84c1cfef10cb836ab281b38956fecf63c Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:53:37 -0500 Subject: [PATCH 04/26] Use MySQL ->> operator for resource_title JSON search Switches from JSON_UNQUOTE(JSON_EXTRACT(...)) to the ->> shorthand for more reliable JSON path extraction. Co-Authored-By: Claude Opus 4.6 --- app/controllers/admin/ahoy_activities_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/ahoy_activities_controller.rb b/app/controllers/admin/ahoy_activities_controller.rb index 14be54e3f..a299e2ca3 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -54,7 +54,7 @@ def index 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 ?", + "ahoy_events.properties->>'$.resource_title' LIKE ?", "%#{term}%" ) end From 70cc2e0974448588280c265b2bbc016e2713e6fe Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:57:25 -0500 Subject: [PATCH 05/26] Update filter badges dynamically via turbo_stream.replace Wrap active_filters_subheader in an identifiable div so turbo stream can replace it on each search. Search box filters (event name, resource, visit ID, props, user) now appear as chips alongside the existing time period and audience badges. Also add user filter badge. Co-Authored-By: Claude Opus 4.6 --- .../ahoy_activities/_event_results.html.erb | 1 + .../ahoy_activities/_visit_results.html.erb | 1 + .../shared/_active_filters_subheader.html.erb | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/views/admin/ahoy_activities/_event_results.html.erb b/app/views/admin/ahoy_activities/_event_results.html.erb index bb62569d6..abc0feeb7 100644 --- a/app/views/admin/ahoy_activities/_event_results.html.erb +++ b/app/views/admin/ahoy_activities/_event_results.html.erb @@ -1,4 +1,5 @@ <%= 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_icon = ->(column) { diff --git a/app/views/admin/ahoy_activities/_visit_results.html.erb b/app/views/admin/ahoy_activities/_visit_results.html.erb index cc746bf4a..b2e4d0232 100644 --- a/app/views/admin/ahoy_activities/_visit_results.html.erb +++ b/app/views/admin/ahoy_activities/_visit_results.html.erb @@ -1,4 +1,5 @@ <%= turbo_stream.replace("visit_count", partial: "visit_count") %> +<%= turbo_stream.replace("active_filters_subheader", partial: "admin/shared/active_filters_subheader") %> <% sort_base = params.permit(:user_id, :visit_id, :time_period, :from, :to, :per_page, audience: []).to_h.symbolize_keys sort_icon = ->(column) { diff --git a/app/views/admin/shared/_active_filters_subheader.html.erb b/app/views/admin/shared/_active_filters_subheader.html.erb index 37aa33a0d..830555e9f 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -1,3 +1,4 @@ +
<% time_period = params[:time_period].presence || "past_month" time_label = case time_period @@ -16,9 +17,13 @@ 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] } + + has_search_filters = params[:name].present? || params[:resource_name].present? || + params[:visit_id].present? || params[:props].present? || + params[:user_id].present? %> -<% if time_label || audience_labels.any? %> -
+<% if time_label || audience_labels.any? || has_search_filters %> +
Filtering: <% if time_label %> @@ -45,5 +50,14 @@ Props: <%= params[:props] %> <% end %> + <% if params[:user_id].present? %> + <% user = User.find_by(id: params[:user_id]) %> + <% if user %> + + User: <%= user.full_name %> + + <% end %> + <% end %>
<% end %> +
From 34b611b62e7bc1907a8287e2c9e69582c8ff6f9a Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 06:58:57 -0500 Subject: [PATCH 06/26] Add from/to date chips matching time period color Co-Authored-By: Claude Opus 4.6 --- .../admin/shared/_active_filters_subheader.html.erb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 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 830555e9f..d8605b735 100644 --- a/app/views/admin/shared/_active_filters_subheader.html.erb +++ b/app/views/admin/shared/_active_filters_subheader.html.erb @@ -20,7 +20,8 @@ has_search_filters = params[:name].present? || params[:resource_name].present? || params[:visit_id].present? || params[:props].present? || - params[:user_id].present? + params[:user_id].present? || params[:from].present? || + params[:to].present? %> <% if time_label || audience_labels.any? || has_search_filters %>
@@ -50,6 +51,16 @@ 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 %> From 5f15409b861f41f6d5cd75e97067e2ffa683c71c Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:01:04 -0500 Subject: [PATCH 07/26] Auto-submit date fields on change Date inputs have type="date" which the collection controller doesn't handle, so add explicit onchange to trigger form submission when a date is picked. Co-Authored-By: Claude Opus 4.6 --- app/views/admin/ahoy_activities/index.html.erb | 6 ++++-- app/views/admin/ahoy_activities/visits.html.erb | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 93db64c9d..629153c5d 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -228,7 +228,8 @@ <%= 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", + onchange: "this.form.requestSubmit()" %>
@@ -236,7 +237,8 @@ <%= 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", + onchange: "this.form.requestSubmit()" %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 3ccfb843a..47c21a971 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -73,7 +73,8 @@ <%= 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", + onchange: "this.form.requestSubmit()" %>
@@ -81,7 +82,8 @@ <%= 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", + onchange: "this.form.requestSubmit()" %>
From d1550cec6ea7980764a09ea9268314a716ef58e1 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:02:22 -0500 Subject: [PATCH 08/26] Handle date and number inputs in collection controller Add date to the change handler and number to the input handler so the Stimulus controller auto-submits for all input types. Remove redundant inline oninput/onchange attributes from the views. Also clear date and number fields in clearAndSubmit. Co-Authored-By: Claude Opus 4.6 --- .../controllers/collection_controller.js | 7 +- .../admin/ahoy_activities/index.html.erb | 151 ++---------------- .../admin/ahoy_activities/visits.html.erb | 6 +- 3 files changed, 20 insertions(+), 144 deletions(-) diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js index b9f56ce46..ff15c9868 100644 --- a/app/frontend/javascript/controllers/collection_controller.js +++ b/app/frontend/javascript/controllers/collection_controller.js @@ -15,13 +15,14 @@ export default class extends Controller { type === "checkbox" || type === "radio" || type === "select-one" || - type === "select-multiple" + type === "select-multiple" || + type === "date" ) { this.submitForm(); } }); this.element.addEventListener("input", (event) => { - if (event.target.type === "text") { + if (event.target.type === "text" || event.target.type === "number") { this.debouncedSubmit(); } }); @@ -57,7 +58,7 @@ export default class extends Controller { clearAndSubmit(event) { event.preventDefault(); - this.element.querySelectorAll('input[type="text"], input[type="search"]').forEach(input => { + this.element.querySelectorAll('input[type="text"], input[type="search"], input[type="number"], input[type="date"]').forEach(input => { input.value = ''; }); this.element.querySelectorAll('select').forEach(select => { diff --git a/app/views/admin/ahoy_activities/index.html.erb b/app/views/admin/ahoy_activities/index.html.erb index 629153c5d..dfeab830d 100644 --- a/app/views/admin/ahoy_activities/index.html.erb +++ b/app/views/admin/ahoy_activities/index.html.erb @@ -51,142 +51,17 @@ 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", - oninput: "this.form.requestSubmit()" %> + %>
- <%= render "admin/shared/activities_tabs" %> - - <%= render "admin/shared/active_filters_subheader" %> - - -
- <%= form_with url: request.path, method: :get, local: true do %> - -
- - -
- - <%= text_field_tag :name, - params[:name], - placeholder: "e.g. Viewed Workshop", - 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" %> -
- - -
- - <%= select_tag :user_id, - options_from_collection_for_select( - User.where(id: Ahoy::Visit.select(:user_id).distinct).order(:first_name, :last_name), - :id, - :full_name, - params[:user_id] - ), - include_blank: "All Users", - class: "w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %> -
- - -
- <%= render "admin/shared/audience_dropdown", auto_submit: false, stacked: true %> -
- - -
- - <%= 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" - ), - 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" %> -
- -
- - -
- Quick: - <% safe_params = params.slice(:name, :resource_name, :visit_id, :props, :user_id, :from, :to).to_unsafe_h %> - <%= link_to "24h", - url_for(safe_params.merge(from: 1.day.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "7d", - url_for(safe_params.merge(from: 7.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> - - <%= link_to "30d", - url_for(safe_params.merge(from: 30.days.ago.to_date)), - class: "text-indigo-600 hover:underline" %> -
- -
- <%= submit_tag "Filter", - class: "bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition" %> -
- - <% end %> + +
+ + <%= 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", + %>
@@ -196,7 +71,7 @@ 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", - oninput: "this.form.requestSubmit()" %> + %>
@@ -206,7 +81,7 @@ 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", - oninput: "this.form.requestSubmit()" %> + %>
@@ -229,7 +104,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()" %> + %>
@@ -238,7 +113,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()" %> + %>
diff --git a/app/views/admin/ahoy_activities/visits.html.erb b/app/views/admin/ahoy_activities/visits.html.erb index 47c21a971..6dd30b144 100644 --- a/app/views/admin/ahoy_activities/visits.html.erb +++ b/app/views/admin/ahoy_activities/visits.html.erb @@ -65,7 +65,7 @@ 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", - oninput: "this.form.requestSubmit()" %> + %>
@@ -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 cd240507fbfbac0ec11fd1a5b3a915da72330bb5 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:07:11 -0500 Subject: [PATCH 09/26] 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 ba42cf07314e4ef93b0cef75a5008bb9f3877c06 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:11:30 -0500 Subject: [PATCH 10/26] 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 | 63 +++++++++++++------ 4 files changed, 119 insertions(+), 52 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 d8605b735..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,48 +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 5e1ce66bdab68b62fb711c040d8dabb93f641675 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:12:46 -0500 Subject: [PATCH 11/26] 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 e44d9156367128f0cfc8b0e5a6d29a1caccdfe37 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:13:24 -0500 Subject: [PATCH 12/26] 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 5fd066dd191bd76b2d0700eed14c94836d371f27 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:15:01 -0500 Subject: [PATCH 13/26] 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 e5f9979269d2d30c24f976540f31bf4aa172610d Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:21:29 -0500 Subject: [PATCH 14/26] 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 b1732c447..f171475f2 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 3a44c52d32478889027fda0d714be9b788751a44 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:22:52 -0500 Subject: [PATCH 15/26] 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 f171475f2..3595096be 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 a033e575fefebe00b083afb0a9705c562b8a2441 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:25:09 -0500 Subject: [PATCH 16/26] 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 3a86b9300d5f67ef97d2889698966ef9b9e26278 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:26:08 -0500 Subject: [PATCH 17/26] 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 3595096be..1c8875dd5 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 64d02f6effbe66585f3c76ed15bd633f320d7b18 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:27:19 -0500 Subject: [PATCH 18/26] 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 ddc81149c0f7933e9d61489b837b596eb20179b8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:27:59 -0500 Subject: [PATCH 19/26] 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 1c8875dd5..21f375067 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 14f5cfa0ebb6240591461edb4a9d47f727b2e2fa Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:30:01 -0500 Subject: [PATCH 20/26] 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 e1410f3d2f8548df86d40dcd4bbedf20459bff86 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:32:50 -0500 Subject: [PATCH 21/26] 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 e456a1b5924f9c7151d90e8323146fb0c3f4b9e8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:35:33 -0500 Subject: [PATCH 22/26] 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 393cd07ce5679363d60bb17a887cb4f6e4e7581b Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 07:46:14 -0500 Subject: [PATCH 23/26] 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 27d037dde332639c9e2dee549df9b46339d8f2f3 Mon Sep 17 00:00:00 2001 From: maebeale Date: Fri, 6 Mar 2026 18:53:33 -0500 Subject: [PATCH 24/26] 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 a299e2ca3..fc73be220 100644 --- a/app/controllers/admin/ahoy_activities_controller.rb +++ b/app/controllers/admin/ahoy_activities_controller.rb @@ -97,18 +97,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 @@ -147,15 +147,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( - "JSON_UNQUOTE(JSON_EXTRACT(ahoy_events.properties, '$.resource_title')) LIKE ?", + "LOWER(ahoy_events.properties->>'$.resource_title') LIKE LOWER(?)", "%#{term}%" ) end @@ -209,6 +209,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 48d8f2f28..1b922b2fa 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) @@ -42,6 +45,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 da0b98729..887e33f18 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 5fa53428d..c48b3e108 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