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
89
90
91
92
require "pro_tacts/admin/phlex"
require "pro_tacts/admin/group_filter"
require "pro_tacts/admin/group_label"
module ProTacts
module Admin
# The groups dialog on a contact's card, opened by the groups row's
# "edit": ContactDialog's popover, over every group as a
# checkbox, the contact's own checked. The save sends what the page
# loaded with beside what it sends back (`was[]`), so the route can
# move only what was toggled (Web#apply_groups).
#
# The filter over those boxes, the cap on how many stand shown, the
# button that lifts it, and the offer to make the group the filter
# names are Admin::GroupFilter's, shared with the import's own
# picker (Admin::ImportGroups). The filter box sits outside the
# form here, because Enter in a form's text box would save
# mid-filter and there is somewhere outside to put it.
#
# Closing the popover puts the cap, the filter, and any names
# ticked into making back. Alpine's state belongs to the element,
# and a popover is hidden rather than rebuilt, so without the
# reset a dialog reopens on whatever the last visit left in it — a
# filter typed ten minutes ago, the whole list still unrolled, or
# a name committed and then left there by a Cancel.
#
# `land` is where a save returns to, other than the contact's own
# page: the walk's saved row opens this dialog, and a look back at
# a row is not a navigation away from the walk — the row's own
# screen is where both a save and a refusal belong
# (Web#apply_groups).
class GroupDialog < Phlex::HTML
ID = "edit-groups" #: String
FORM = "edit-groups-form" #: String
# @rbs @contact: Contact
# @rbs @groups: Array[Store::GroupChoice]
# @rbs @joined: Array[String]
# @rbs @capped: Array[String]
# @rbs @land: String?
# `GroupChoice#<=>`'s order rather than the id order a tag
# keeps — a list to find a name in, and nothing here stays put
# across a rename. `groups` is every group as a
# choice — no members read, the book's whole membership not
# being this dialog's business (Store#group_choices) — and
# `joined` the ids this contact is already in, which the show
# screen already holds (Store#groups_of) and so does not read a
# second time. `joined` is also what the cap spares: those are
# the boxes this is opened to untick.
#: (contact: Contact, groups: Array[Store::GroupChoice], joined: Array[String], ?land: String?) -> void
def initialize(contact:, groups:, joined:, land: nil)
@contact = contact
@groups = groups.sort
@joined = joined
@land = land
@capped = GroupFilter.capped(@groups.map(&:id), joined:)
end
def view_template
dialog(id: ID, popover: "auto", x_data: GroupFilter::STATE,
"@toggle": "if ($event.newState === 'closed') { all = false; filter = ''; named = [] }") do
header { "Groups" }
div(class: "field-stack") do
render GroupFilter.new(autofocus: true)
form(id: FORM, action: "/contacts/#{@contact.id}/groups", method: "post",
class: "field-stack", x_ref: "options") do
input(type: "hidden", name: "land", value: @land) if @land
@groups.each do |group|
joined = @joined.include?(group.id)
label(**GroupFilter.row(group.label, capped: @capped.include?(group.id))) do
input(type: "checkbox", name: "groups[]", value: group.id, checked: joined)
render GroupLabel.new(group:)
end
input(type: "hidden", name: "was[]", value: group.id) if joined
end
render GroupFilter::Named.new
render GroupFilter::Fresh.new
end
render GroupFilter::Empty.new(any: @groups.any?)
render GroupFilter::Rest.new(count: @groups.length) if @capped.any?
end
footer do
button(type: "button", popovertarget: ID, popovertargetaction: "hide") { "Cancel" }
button(type: "submit", form: FORM, data: {variant: "primary"}) { "Save" }
end
end
end
end
end
end