1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
require "pro_tacts/admin/phlex"

require "pro_tacts/admin/avatar"
require "pro_tacts/admin/format"
require "pro_tacts/admin/group_filter"
require "pro_tacts/admin/layout"
require "pro_tacts/admin/record_card"

module ProTacts
  module Admin
    # A group's members picked from the whole book, the groups
    # dialog's mirror (Admin::GroupDialog): a box per contact, ticked
    # for the ones in the group. A screen and not the dialog's popover
    # for the reason the cap gives from the other side: a contact's
    # groups are a handful, a group's members are the whole book, and a
    # page scrolls where a popover clips. The list is the show screen's
    # own (admin.css, the rows carrying a checkbox where a row's link
    # stands), so the screen reads as that list made editable rather
    # than as a form's option stack (Admin::GroupsShow).
    #
    # The form owns the picker's Alpine state and so wraps both
    # cards, the record's own above and the list under it — $refs and
    # the state reach only inside their x-data element — which puts
    # the filter inside the form it filters: Enter in it is guarded
    # rather than submitted, the import's own answer where there is
    # no outside to stand in (Admin::GroupFilter), whose filter, cap,
    # and show-all button these are, without its offer.
    #
    # Two screens are this one: a group's (Admin::GroupsMembers) and a
    # group's not yet made (Admin::GroupsNew). Each names its title,
    # its form's action, its way back, its submit, the boxes ticked,
    # and `was`, what the page loaded with, sent back so the save can
    # move only what was toggled (Web#apply_members).
    class MemberPicker < Phlex::HTML
      # @rbs @login: String
      # @rbs @notice: String?
      # @rbs @contacts: Array[ListedContact]
      # @rbs @capped: Array[String]

      # The form's id, for the footer's submit outside it.
      FORM = "members-form" #: String
      private_constant :FORM

      #: (contacts: Array[ListedContact], login: String, ?notice: String?) -> void
      def initialize(contacts:, login:, notice: nil)
        @login = login
        @notice = notice
        # The order the show screen's list reads (Contact#<=>),
        # which is the order the book is browsed in everywhere else.
        @contacts = contacts.sort
        # The ticked are never capped (GroupFilter.capped).
        @capped = GroupFilter.capped(@contacts.map(&:id), joined: ticked)
      end

      def view_template
        render Layout.new(title:, login: @login, notice: @notice) do
          form(action:, method: "post", class: "member-picker", id: FORM, x_data: GroupFilter::STATE) do
            render RecordCard.new(back:, submit:, form: FORM) do
              render GroupFilter.new(autofocus: true, in_form: true, placeholder: "Filter contacts")
            end
            div(class: "member-list") do
              ul(class: "card", x_ref: "options") do
                @contacts.each do |contact|
                  # The row's label is the name the lists show
                  # (Format.name_label), nickname and all, because
                  # that is the name a person filters by; the row
                  # itself is the li, so the cap hides the whole of
                  # it and not the box alone (Admin::GroupFilter).
                  li(**GroupFilter.row(Format.name_label(contact),
                                       capped: @capped.include?(contact.id))) do
                    label do
                      input(type: "checkbox", name: "members[]", value: contact.id, checked: ticked.include?(contact.id))
                      render Avatar.new(contact:, size: "lg")
                      div(style: "flex: 1; min-width: 0; font-weight: 550;") { Format.name_label(contact) }
                    end
                    input(type: "hidden", name: "was[]", value: contact.id) if was.include?(contact.id)
                  end
                end
              end
              render GroupFilter::Empty.new(any: @contacts.any?, offer: false, noun: "contacts")
              render GroupFilter::Rest.new(count: @contacts.length, noun: "contacts") if @capped.any?
            end
          end
        end
      end
    end
  end
end