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