admin: a nameless group's id stops passing for a name
A group with no name was shown by its id in the name's own type,
so it looked like someone had named the group "3f9a".
GroupLabel now sets that id in mono, quieter, at regular weight,
wherever a group is named: headings, rows, tags, and badges.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/lib/pro_tacts/admin/contacts_index.rb b/lib/pro_tacts/admin/contacts_index.rb
index e45173f..e0bd151 100644
--- a/lib/pro_tacts/admin/contacts_index.rb
+++ b/lib/pro_tacts/admin/contacts_index.rb
@@ -3,6 +3,7 @@ require "pro_tacts/admin/phlex"
require "pro_tacts/admin/avatar"
require "pro_tacts/admin/contact_dialog"
require "pro_tacts/admin/format"
+require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
require "pro_tacts/admin/upcoming_birthdays"
@@ -117,7 +118,7 @@ module ProTacts
@groups.each do |group|
li do
a(href: "/groups/#{group.id}") do
- div(style: "flex: 1; min-width: 0; font-weight: 550;") { group.label }
+ div(style: "flex: 1; min-width: 0; font-weight: 550;") { render GroupLabel.new(group:) }
end
end
end
diff --git a/lib/pro_tacts/admin/contacts_show.rb b/lib/pro_tacts/admin/contacts_show.rb
index 576fbfb..a870901 100644
--- a/lib/pro_tacts/admin/contacts_show.rb
+++ b/lib/pro_tacts/admin/contacts_show.rb
@@ -2,6 +2,7 @@ require "pro_tacts/admin/phlex"
require "pro_tacts/admin/avatar"
require "pro_tacts/admin/format"
+require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
module ProTacts
@@ -159,7 +160,7 @@ module ProTacts
dt(class: "type-label") { "groups" }
dd(class: "type-body-sm") do
div(class: "tag-set") do
- @groups.each { |group| a(href: "/groups/#{group.id}", class: "tag") { group.label } }
+ @groups.each { |group| a(href: "/groups/#{group.id}", class: "tag") { render GroupLabel.new(group:) } }
end
end
end
@@ -182,10 +183,19 @@ module ProTacts
# value column (admin.css) — under the value it could be
# read as marking the row below. No tone: the accent is held
# constant across a view, and inherited is not a status.
- span(class: "badge") { group } if group
+ span(class: "badge") { group_mark(group) } if group
end
end
+ # A lent row knows its group only by label (Contact#group_of), and
+ # a nameless group's label is its id — so the id is looked up among
+ # the contact's own groups to mark it the way GroupLabel does.
+ #: (String label) -> void
+ def group_mark(label)
+ nameless = @groups.find { it.name.nil? && it.id == label }
+ nameless ? render(GroupLabel.new(group: nameless)) : plain(label)
+ end
+
#: (String | Array[String] value) -> void
def render_value(value)
if value.is_a?(Array)
diff --git a/lib/pro_tacts/admin/group_label.rb b/lib/pro_tacts/admin/group_label.rb
new file mode 100644
index 0000000..a63378d
--- /dev/null
+++ b/lib/pro_tacts/admin/group_label.rb
@@ -0,0 +1,27 @@
+require "pro_tacts/admin/phlex"
+
+module ProTacts
+ module Admin
+ # A group's name wherever one is shown. A group needs no name
+ # (db/migrations/005_group_identity.rb), and one without is shown by
+ # its id, marked `data-nameless` so admin.css can set it as an
+ # identifier rather than let it pass for a name.
+ class GroupLabel < Phlex::HTML
+ # @rbs @group: Store::Group
+
+ #: (group: Store::Group) -> void
+ def initialize(group:)
+ @group = group
+ end
+
+ def view_template
+ name = @group.name
+ if name
+ plain name
+ else
+ span(data: {nameless: true}) { @group.id }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pro_tacts/admin/groups_edit.rb b/lib/pro_tacts/admin/groups_edit.rb
index 002dd97..c45aff0 100644
--- a/lib/pro_tacts/admin/groups_edit.rb
+++ b/lib/pro_tacts/admin/groups_edit.rb
@@ -1,6 +1,7 @@
require "pro_tacts/admin/phlex"
require "pro_tacts/admin/contacts_edit"
+require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
module ProTacts
@@ -38,7 +39,10 @@ module ProTacts
# add button sits, and the row it reveals is inside the form.
div(class: "record", x_data: "{ adding: false }") do
div(class: "record-nav") do
- a(href: "/groups/#{@group.id}", class: "type-label") { "‹ #{@group.label}" }
+ a(href: "/groups/#{@group.id}", class: "type-label") do
+ plain "‹ "
+ render GroupLabel.new(group: @group)
+ end
button(type: "button", data_size: "sm", "x-show": "!adding",
"@click": "adding = true") { "add address" }
end
diff --git a/lib/pro_tacts/admin/groups_index.rb b/lib/pro_tacts/admin/groups_index.rb
index aaae82c..7039b2a 100644
--- a/lib/pro_tacts/admin/groups_index.rb
+++ b/lib/pro_tacts/admin/groups_index.rb
@@ -1,5 +1,6 @@
require "pro_tacts/admin/phlex"
+require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
module ProTacts
@@ -35,7 +36,7 @@ module ProTacts
@groups.each do |group|
li do
a(href: "/groups/#{group.id}") do
- div(style: "flex: 1; min-width: 0; font-weight: 550;") { group.label }
+ div(style: "flex: 1; min-width: 0; font-weight: 550;") { render GroupLabel.new(group:) }
span(class: "type-label") { members_label(group) }
end
end
diff --git a/lib/pro_tacts/admin/groups_show.rb b/lib/pro_tacts/admin/groups_show.rb
index 26db87b..587f4c7 100644
--- a/lib/pro_tacts/admin/groups_show.rb
+++ b/lib/pro_tacts/admin/groups_show.rb
@@ -1,6 +1,7 @@
require "pro_tacts/admin/phlex"
require "pro_tacts/admin/format"
+require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
module ProTacts
@@ -33,7 +34,7 @@ module ProTacts
div(class: "card") do
div(class: "card-body") do
div(class: "detail-header") do
- h1(class: "type-h2", style: "margin: 0;") { @group.label }
+ h1(class: "type-h2", style: "margin: 0;") { render GroupLabel.new(group: @group) }
end
# Never empty, unlike ContactsShow's: the members row
# always renders (#members_row).
diff --git a/public/admin.css b/public/admin.css
index 43ed65c..fb17ead 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -579,6 +579,16 @@ img.avatar {
.detail-grid dd:has(> a.type-label) > div {
grid-column: 1;
}
+/* A nameless group shown by its id (Admin::GroupLabel): set in mono,
+ a step quieter, and at regular weight even inside a bold heading or
+ row, so it reads as an identifier standing in for a name rather than
+ as the name itself. */
+[data-nameless] {
+ font-family: var(--gl-font-mono);
+ font-weight: 400;
+ color: var(--gl-color-text-secondary);
+}
+
/* A row's action — a group card's "edit members" — takes the badge's
place at the row's right edge. Unlike the badge it is plain text,
so it has a real baseline to share with the tags beside it. */
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 416d073..4d67830 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -308,9 +308,9 @@ class AdminContactsPagesTest < Minitest::Test
nameless = FixtureData.seed_group(store, members: ["ada"], lines: [])
get "/contacts/ada"
- tags = last_response.body.scan(%r{<a href="/groups/([^"]*)" class="tag">([^<]*)</a>})
+ tags = last_response.body.scan(%r{<a href="/groups/([^"]*)" class="tag">(.*?)</a>})
- assert_equal({named => "Booles", nameless => nameless}.sort, tags)
+ assert_equal({named => "Booles", nameless => %(<span data-nameless>#{nameless}</span>)}.sort, tags)
# The grid leads with what the contact belongs to, so the values
# under it are read already knowing whose they might be.
assert_equal "groups", last_response.body[%r{<dt class="type-label">([^<]*)<}, 1]
@@ -335,9 +335,9 @@ class AdminContactsPagesTest < Minitest::Test
id = FixtureData.seed_group(store, members: ["ada"], lines: HOUSEHOLD)
get "/contacts/ada"
- marks = last_response.body.scan(%r{<span class="badge">([^<]*)</span>})
+ marks = last_response.body.scan(%r{<span class="badge">(.*?)</span></dd>})
- assert_equal [[id], [id]], marks
+ assert_equal [[%(<span data-nameless>#{id}</span>)]] * 2, marks
end
end