Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/models/sector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ class Sector < ApplicationRecord
scope :sector_name, ->(sector_name) {
sector_name.present? ? where("sectors.name LIKE ?", "%#{sector_name}%") : all }
scope :sector_ids, ->(ids) { where(id: ids.to_s.split("-").map(&:to_i)) }
scope :sector_names_all, ->(names) do
return all if names.blank?
parsed = Array(names).flat_map { |n| n.to_s.split("--") }.map(&:strip).reject(&:blank?).map(&:downcase)
return all if parsed.empty?
where("LOWER(sectors.name) IN (?)", parsed)
end
scope :excluding_other, -> { where.not(name: OTHER_SECTOR_NAME) }
scope :has_taggings, -> { joins(:sectorable_items).distinct }
scope :has_published_taggings, -> {
Expand Down
24 changes: 21 additions & 3 deletions app/views/categories/_tagging_label.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,42 @@

<% name_only ||= false %>

<%# Tags hidden from the public (unpublished) are only ever shown to admins, so
they read as a muted grey chip with an eye-slash icon — the same "hidden" cue
used on password fields — to flag at a glance what the public can't see. %>
<% is_hidden = category.respond_to?(:published?) && !category.published? %>

<% bg_color ||= DomainTheme.bg_class_for(:sectors) %>

<%# bg_hover_color ||= DomainTheme.bg_class_for(:sectors, hover: true) %>

<% bg_hover_color ||= "bg-lime-200" %>

<% text_color = "text-gray-500" %>

<% border_class = DomainTheme.border_class_for(:categories) %>

<% border_hover_class = DomainTheme.border_class_for(:categories, intensity: 500) %>

<% if is_hidden
bg_color = "bg-gray-100"
bg_hover_color = "bg-gray-200"
border_class = "border-gray-200"
border_hover_class = "border-gray-300"
end %>

<% if category %>
<%= link_to taggings_path(category_names_all: category.name),
data: { turbo_frame: "_top", category_name: category.name, controller: "tag-link-loading", action: "click->tag-link-loading#showSpinner" },
class: "inline-flex items-center
rounded-md
border #{ DomainTheme.border_class_for(:categories) } hover:#{ DomainTheme.border_class_for(:categories, intensity: 500) }
border #{ border_class } hover:#{ border_hover_class }
#{ bg_color } hover:#{ bg_hover_color }
text-gray-500
#{ text_color }
px-3 py-1
text-sm font-medium
transition" do %>
<span data-tag-link-loading-target="text"><%= "#{category.category_type&.name&.titleize}: " if !name_only && category.category_type %><%= category.name %></span>
<span data-tag-link-loading-target="text"><% if is_hidden %><%= render "shared/hidden_tag_icon" %><% end %><%= "#{category.category_type&.name&.titleize}: " if !name_only && category.category_type %><%= category.name %></span>
<span data-tag-link-loading-target="spinner" class="hidden"><i class="fa-solid fa-spinner animate-spin" aria-hidden="true"></i></span>
<% end %>
<% end %>
21 changes: 19 additions & 2 deletions app/views/sectors/_tagging_label.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

<% is_primary ||= false %>

<%# Tags hidden from the public (unpublished) are only ever shown to admins, so
they read as a muted grey chip with an eye-slash icon — the same "hidden" cue
used on password fields — to flag at a glance what the public can't see. %>
<% is_hidden = sector.respond_to?(:published?) && !sector.published? %>

<%# Primary service areas read as a darker-green chip with a star so they stand
out from the additional sectors, matching the star used in the chip editor;
everyone else keeps the light-lime default. %>
Expand All @@ -17,18 +22,30 @@

<% text_color ||= is_primary ? "text-lime-800" : "text-gray-500" %>

<% border_class = DomainTheme.border_class_for(:sectors) %>

<% border_hover_class = DomainTheme.border_class_for(:sectors, intensity: 500) %>

<% if is_hidden

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: The grey hidden treatment intentionally overrides any explicit bg_color/border passed in (e.g. the bg-white chips on /tags), so hidden always reads as hidden regardless of caller styling.

bg_color = "bg-gray-100"
bg_hover_color = "bg-gray-200"
text_color = "text-gray-500"
border_class = "border-gray-200"
border_hover_class = "border-gray-300"
end %>

<% if sector %>
<%= link_to taggings_path(sector_names_all: sector.name),
data: { turbo_frame: "_top", sector_name: sector.name, controller: "tag-link-loading", action: "click->tag-link-loading#showSpinner" },
class: "inline-flex items-center
rounded-md
border #{ DomainTheme.border_class_for(:sectors) } hover:#{ DomainTheme.border_class_for(:sectors, intensity: 500) }
border #{ border_class } hover:#{ border_hover_class }
#{ bg_color } hover:#{ bg_hover_color }
#{ text_color }
px-3 py-1
text-sm font-medium
transition" do %>
<span data-tag-link-loading-target="text"><% if is_primary %><i class="fa-solid fa-star mr-1 text-xs text-amber-400" aria-hidden="true" title="Primary service area"></i><% end %><%= sector.name %><% if display_leader && is_leader %><span class="ml-1 text-xs text-indigo-600">(Leader)</span><% end %></span>
<span data-tag-link-loading-target="text"><% if is_hidden %><%= render "shared/hidden_tag_icon" %><% end %><% if is_primary %><i class="fa-solid fa-star mr-1 text-xs text-amber-400" aria-hidden="true" title="Primary service area"></i><% end %><%= sector.name %><% if display_leader && is_leader %><span class="ml-1 text-xs text-indigo-600">(Leader)</span><% end %></span>
<span data-tag-link-loading-target="spinner" class="hidden"><i class="fa-solid fa-spinner animate-spin" aria-hidden="true"></i></span>
<% end %>
<% end %>
4 changes: 4 additions & 0 deletions app/views/shared/_hidden_tag_icon.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<%# Closed-eye icon — the same "hidden" glyph used by the password-visibility
toggle on the login pages — flagging a tag that's hidden from the public and
visible only to admins. %>
<svg class="mr-1 inline-block size-3 align-[-0.125em]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><title>Hidden from the public</title><path d="M2 10c3 4 6.5 6 10 6s7-2 10-6"/><path d="M7.5 14l-1.5 3"/><path d="M12 16v3"/><path d="M16.5 14l1.5 3"/></svg>
42 changes: 30 additions & 12 deletions app/views/taggings/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<% content_for(:page_bg_class, "public") %>
<% sector_names = @sector_names_all.to_s.split("--").join(", ") %>
<%# Resolve the active filters to records so admins can jump straight to editing
the sector/category they're browsing. The header still falls back to the raw
param names below for any value that doesn't match a record. %>
<% filtered_sectors = @sector_names_all.present? ? Sector.sector_names_all(@sector_names_all).order(:name).to_a : [] %>
<% filtered_categories = @category_names_all.present? ? Category.category_names_all(@category_names_all).includes(:category_type).order(:name).to_a : [] %>
<% category_full_names =
if @category_names_all.present?
Category.category_names_all(@category_names_all)
.includes(:category_type)
.map { |c| "#{c.category_type&.name&.titleize}: #{c.name}" }
.join(", ")
else
nil
end
filtered_categories.map { |c| "#{c.category_type&.name&.titleize}: #{c.name}" }.join(", ").presence
%>

<div class="taggings-index max-w-7xl mx-auto <%= DomainTheme.bg_class_for(:tags) %> border border-gray-200 rounded-xl shadow p-6">
Expand All @@ -29,14 +27,34 @@
<% if sector_names.present? || category_full_names.present? || @category_names_all.present? %>
<div class="mt-4 text-center">
<% if sector_names.present? %>
<div class="text-3xl md:text-4xl font-semibold text-gray-900 leading-tight">
<%= sector_names %>
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-2">
<span class="text-3xl md:text-4xl font-semibold text-gray-900 leading-tight">
<%= sector_names %>
</span>
<% if filtered_sectors.any? && allowed_to?(:edit?, filtered_sectors.first) %>
<% filtered_sectors.each do |sector| %>
<%= link_to edit_sector_path(sector),
class: "admin-only bg-blue-100 btn btn-secondary-outline whitespace-nowrap" do %>
Edit sector<%= " (#{sector.name})" if filtered_sectors.size > 1 %>
<% end %>
<% end %>
<% end %>
</div>
<% end %>

<% if category_full_names.present? || @category_names_all.present? %>
<div class="mt-2 text-base md:text-lg text-gray-600 font-semibold">
<%= category_full_names || @category_names_all %>
<div class="mt-2 flex flex-wrap items-center justify-center gap-x-3 gap-y-2">
<span class="text-base md:text-lg text-gray-600 font-semibold">
<%= category_full_names || @category_names_all %>
</span>
<% if filtered_categories.any? && allowed_to?(:edit?, filtered_categories.first) %>
<% filtered_categories.each do |category| %>
<%= link_to edit_category_path(category),
class: "admin-only bg-blue-100 btn btn-secondary-outline whitespace-nowrap" do %>
Edit category<%= " (#{category.name})" if filtered_categories.size > 1 %>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
</div>
Expand Down
2 changes: 1 addition & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def find_or_create_by_name!(klass, name, **attrs, &block)
# taggings rather than destroying them. SECTOR_TYPES already includes the "Other"
# catch-all, so it stays published.
canonical_names = Sector::SECTOR_TYPES.map(&:downcase)
Sector.reject { |sector| canonical_names.include?(sector.name.downcase) }
Sector.all.reject { |sector| canonical_names.include?(sector.name.downcase) }
.each { |sector| sector.update!(published: false) }

puts "Creating CategoryTypes/Categories…"
Expand Down
42 changes: 42 additions & 0 deletions db/seeds/dev/workshops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,48 @@
end
puts "Done assigning categories and sectors."

# A few unpublished (non-canonical) sectors that still carry taggings, so the
# taggings admin has data exercising the "tagged but hidden" state. The base
# db/seeds.rb only creates the canonical SECTOR_TYPES; these are dev-only and
# stay unpublished (they're not on the canonical list).
puts "Creating unpublished sectors with taggings…"
workshop_pool = Workshop.all.to_a
[ "Animal-Assisted Therapy", "School Counseling", "Youth Mentorship" ].each do |sector_name|
sector = Sector.where("LOWER(name) = LOWER(?)", sector_name).first_or_create!(name: sector_name)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Inline case-insensitive first_or_create! rather than the find_or_create_by_name! helper in seeds.rb — this dev file is meant to be runnable standalone (rake db:seed:workshops), where that helper isn't loaded.

sector.update!(published: false)

workshop_pool.sample(rand(2..4)).each do |workshop|
SectorableItem.find_or_create_by!(
sector_id: sector.id,
sectorable_type: "Workshop",
sectorable_id: workshop.id
)
end
end
puts "Done creating unpublished sectors with taggings."

# A few unpublished (non-canonical) categories that still carry taggings, so the
# taggings admin has data exercising the "tagged but hidden" state for categories
# too. The base db/seeds.rb only creates canonical categories; these are dev-only,
# attached to an existing type so they group correctly on the tags page, and stay
# unpublished (they're not on any canonical list).
puts "Creating unpublished categories with taggings…"
art_type = CategoryType.where("LOWER(name) = LOWER(?)", "ArtType").first_or_create!(name: "ArtType", published: true)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: These names are deliberately non-canonical, so base db/seeds.rb never republishes them and they stay hidden. Dev seeds run after base seed, so the unpublished state sticks.

[ "Sand Art", "Found-Object Sculpture", "Shadow Puppetry" ].each do |category_name|
category = Category.where("LOWER(name) = LOWER(?)", category_name)
.first_or_create!(name: category_name, category_type: art_type)
category.update!(published: false)

workshop_pool.sample(rand(2..4)).each do |workshop|
CategorizableItem.find_or_create_by!(
category_id: category.id,
categorizable_type: "Workshop",
categorizable_id: workshop.id
)
end
end
puts "Done creating unpublished categories with taggings."

puts "Creating Workshop Variations…"
# rubocop:disable Style/PercentLiteralDelimiters
variations = [
Expand Down
32 changes: 32 additions & 0 deletions spec/requests/taggings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,38 @@
end
end

describe "admin edit buttons" do
let!(:admin) { create(:user, :admin) }

before { sign_in admin }

it "shows a single Edit sector button when filtering by one sector" do
get taggings_path(sector_names_all: sector_2.name)
expect(response.body).to include(edit_sector_path(sector_2))
expect(response.body).to include("Edit sector")
expect(response.body).not_to include("Edit sector (")
end

it "names each sector when filtering by more than one" do
get taggings_path(sector_names_all: "#{sector_1.name}--#{sector_2.name}")
expect(response.body).to include("Edit sector (#{sector_1.name})")
expect(response.body).to include("Edit sector (#{sector_2.name})")
end

it "shows an Edit category button when filtering by a category" do
get taggings_path(category_names_all: category.name)
expect(response.body).to include(edit_category_path(category))
expect(response.body).to include("Edit category")
end
end

describe "edit buttons for non-admins" do
it "does not show edit buttons to a regular signed-in user" do
get taggings_path(sector_names_all: sector_2.name)
expect(response.body).not_to include("Edit sector")
end
end

describe "when no matching tags exist" do
it "does not blow up and renders empty sections" do
get taggings_path(sector_names_all: "Nonexistent")
Expand Down
13 changes: 13 additions & 0 deletions spec/views/categories/_tagging_label.html.erb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,17 @@

expect(rendered).not_to include("Theme:")
end

context "when the category is hidden from the public (unpublished)" do
let(:category) { create(:category, :unpublished, name: "Resilience", category_type: type) }

it "renders a muted grey chip with the closed-eye hidden icon" do
render partial: "categories/tagging_label",
locals: { category: category, name_only: true }

expect(rendered).to include("Hidden from the public")
expect(rendered).to include("bg-gray-100")
expect(rendered).not_to include(DomainTheme.bg_class_for(:sectors))
end
end
end
21 changes: 20 additions & 1 deletion spec/views/sectors/_tagging_label.html.erb_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'rails_helper'

RSpec.describe "sectors/_tagging_label", type: :view do
let(:sector) { create(:sector, name: "Survivors") }
let(:sector) { create(:sector, :published, name: "Survivors") }

it "links to taggings with sector filter" do
render partial: "sectors/tagging_label", locals: { sector: sector }
Expand All @@ -23,4 +23,23 @@
expect(rendered).to include("fa-star")
expect(rendered).to include("bg-lime-200")
end

context "when the sector is hidden from the public (unpublished)" do
let(:sector) { create(:sector, :unpublished, name: "Survivors") }

it "renders a muted grey chip with the closed-eye hidden icon" do
render partial: "sectors/tagging_label", locals: { sector: sector }

expect(rendered).to include("Hidden from the public")
expect(rendered).to include("bg-gray-100")
expect(rendered).not_to include(DomainTheme.bg_class_for(:sectors))
end

it "keeps the grey treatment even when passed an explicit background colour" do
render partial: "sectors/tagging_label", locals: { sector: sector, bg_color: "bg-white" }

expect(rendered).to include("bg-gray-100")
expect(rendered).not_to include("bg-white")
end
end
end