groups: a ticked row is always shown
A hidden box still submits, so a filter that could hide a ticked
one was hiding a decision rather than narrowing a list — the tick
is read off the checkbox at each evaluation, and "No groups match"
follows the rows that stand visible.

Assisted-by: GLM-5.3 via pi
change lrpouurnmzwrstltvllynrzkmrxsqqys
commit caf339b2cec67c8c825b5acdac23de4bb9db15ff
author Alpha Chen <alpha@kejadlen.dev>
date
parent rlvypoxq
diff --git a/lib/pro_tacts/admin/group_filter.rb b/lib/pro_tacts/admin/group_filter.rb
index c593db2..e6acec2 100644
--- a/lib/pro_tacts/admin/group_filter.rb
+++ b/lib/pro_tacts/admin/group_filter.rb
@@ -36,20 +36,30 @@ module ProTacts
       # cap adds to `shows`: a capped row joins the list once the
       # filter is typed into or the whole list is asked for, and a
       # filter is the one of the two that can reach a single group
-      # without the rest. `named` is the names a tick on the offer
-      # committed to making (Named): `creatable` and `none` read it
-      # directly rather than off the rows' `data-label`s, which those
-      # two only re-read when the filter changes — a name committed
-      # since would be invisible to both until the next keystroke.
+      # without the rest. Neither hides a ticked row — a ticked box
+      # submits whether or not it can be seen, so a filter that could
+      # make one vanish would be hiding a decision rather than
+      # narrowing a list — and the tick is read off the checkbox at
+      # each evaluation, so a row answers the next filter change
+      # already ticked. `rows` is every row standing in the options,
+      # the element list `none` reads: no row visible, which is what
+      # "No groups match" claims. `named` is the names a tick on the
+      # offer committed to making (Named): `creatable` and `none`
+      # read it directly rather than off the rows' `data-label`s,
+      # which those two only re-read when the filter changes — a
+      # name committed since would be invisible to both until the
+      # next keystroke.
       STATE = "{ filter: '', all: false, named: [], " \
               "get needle() { return this.filter.trim().toLowerCase() }, " \
               "get labels() { return [...this.$refs.options.querySelectorAll('[data-label]')]" \
               ".map(row => row.dataset.label) }, " \
+              "get rows() { return [...this.$refs.options.querySelectorAll('label')] }, " \
               "shows(label) { return label.includes(this.needle) }, " \
-              "visible(row) { return this.shows(row.dataset.label) && " \
-              "(this.all || this.needle !== '' || !('capped' in row.dataset)) }, " \
-              "get none() { return !this.labels.some(label => this.shows(label)) && " \
-              "!this.named.some(name => name.toLowerCase().includes(this.needle)) }, " \
+              "visible(row) { return row.querySelector('input[type=checkbox]').checked || " \
+              "(this.shows(row.dataset.label) && " \
+              "(this.all || this.needle !== '' || !('capped' in row.dataset))) }, " \
+              "get none() { return !this.named.length && " \
+              "!this.rows.some(row => this.visible(row)) }, " \
               "get creatable() { return this.needle !== '' && !this.labels.includes(this.needle) && " \
               "!this.named.some(name => name.toLowerCase() === this.needle) } }" #: String
 
@@ -140,15 +150,13 @@ module ProTacts
       # independent of the filter — which is the whole difference
       # from the offer, whose row lives on the filter's own text. An
       # untick withdraws the name, back to the offer if the filter
-      # still says it. Never capped, being ticked, and hidden by the
-      # filter like any row — said straight to `shows` rather than
-      # through `data-label`/`visible($el)`, those reading the DOM
-      # the server rendered, and this row being made client-side
-      # where the name is in hand.
+      # still says it. Never capped and never filtered, being ticked
+      # — `visible`'s own rule — so the row needs no attrs to say it:
+      # a bare label, always standing.
       class Named < Phlex::HTML
         def view_template
           template(x_for: "name in named") do
-            label(":hidden": "!shows(name.toLowerCase())") do
+            label do
               input(type: "checkbox", name: "named[]", ":value": "name", checked: true,
                     "@change": "named = named.filter(n => n !== name)")
               span(class: "gl-muted", style: "font-style: italic;", x_text: "name")
diff --git a/lib/pro_tacts/admin/import_groups.rb b/lib/pro_tacts/admin/import_groups.rb
index 64c3649..9129517 100644
--- a/lib/pro_tacts/admin/import_groups.rb
+++ b/lib/pro_tacts/admin/import_groups.rb
@@ -91,9 +91,10 @@ module ProTacts
             # Italic and muted, the shape the groups dialog gives a
             # name that is not a group yet: the box behaves like the
             # ones above it, and the styling is what says this one is
-            # about to be made. Filterable like them too, so that a
-            # name this import is already making is not offered as a
-            # name to make, and never capped, being ticked.
+            # about to be made. Always shown and never capped, being
+            # ticked (`visible`'s own rule); the `data-label` is for
+            # `creatable`, so that a name this import is already
+            # making is not offered as a name to make.
             @named.each do |name|
               label(**GroupFilter.row(name)) do
                 input(type: "checkbox", name: "named[]", value: name, checked: true)
diff --git a/test/pro_tacts/admin/test_import_pages.rb b/test/pro_tacts/admin/test_import_pages.rb
index 790125c..4454238 100644
--- a/test/pro_tacts/admin/test_import_pages.rb
+++ b/test/pro_tacts/admin/test_import_pages.rb
@@ -697,6 +697,11 @@ class AdminImportPagesTest < Minitest::Test
                       %(<input type="checkbox" @change="if ($event.target.checked) named.push(filter.trim())">)
       assert_includes last_response.body,
                       %(<input type="checkbox" name="named[]" :value="name" checked @change="named = named.filter(n => n !== name)">)
+      # A committed name is a ticked row, and a ticked row answers
+      # neither the filter nor the cap: a decision the form still
+      # submits is never hidden.
+      assert_includes last_response.body, %(<template x-for="name in named"><label>)
+      assert_includes last_response.body, %(row.querySelector('input[type=checkbox]').checked)
     end
   end