Commit | Line | Data |
7adfd53f |
1 | package Reaction::UI::ViewPort::ActionForm; |
2 | |
3 | use Reaction::Class; |
4 | |
5 | use aliased 'Reaction::UI::ViewPort::Field::Text'; |
6 | use aliased 'Reaction::UI::ViewPort::Field::Number'; |
7 | use aliased 'Reaction::UI::ViewPort::Field::Boolean'; |
8 | use aliased 'Reaction::UI::ViewPort::Field::File'; |
9 | use aliased 'Reaction::UI::ViewPort::Field::String'; |
10 | use aliased 'Reaction::UI::ViewPort::Field::Password'; |
11 | use aliased 'Reaction::UI::ViewPort::Field::DateTime'; |
12 | use aliased 'Reaction::UI::ViewPort::Field::ChooseOne'; |
13 | use aliased 'Reaction::UI::ViewPort::Field::ChooseMany'; |
14 | use aliased 'Reaction::UI::ViewPort::Field::HiddenArray'; |
15 | use aliased 'Reaction::UI::ViewPort::Field::TimeRange'; |
16 | |
17 | class ActionForm is 'Reaction::UI::ViewPort', which { |
18 | has action => ( |
19 | isa => 'Reaction::InterfaceModel::Action', is => 'ro', required => 1 |
20 | ); |
f670cfd0 |
21 | |
7adfd53f |
22 | has field_names => (isa => 'ArrayRef', is => 'rw', lazy_build => 1); |
f670cfd0 |
23 | |
7adfd53f |
24 | has _field_map => ( |
25 | isa => 'HashRef', is => 'rw', init_arg => 'fields', |
26 | predicate => '_has_field_map', set_or_lazy_build('field_map'), |
27 | ); |
f670cfd0 |
28 | |
7adfd53f |
29 | has changed => ( |
30 | isa => 'Int', is => 'rw', reader => 'is_changed', default => sub { 0 } |
31 | ); |
32 | |
33 | has next_action => ( |
34 | isa => 'ArrayRef', is => 'rw', required => 0, predicate => 'has_next_action' |
35 | ); |
f670cfd0 |
36 | |
7adfd53f |
37 | has on_apply_callback => ( |
38 | isa => 'CodeRef', is => 'rw', required => 0, |
39 | predicate => 'has_on_apply_callback' |
40 | ); |
f670cfd0 |
41 | |
7adfd53f |
42 | has ok_label => ( |
43 | isa => 'Str', is => 'rw', required => 1, default => sub { 'ok' } |
44 | ); |
f670cfd0 |
45 | |
7adfd53f |
46 | has apply_label => ( |
47 | isa => 'Str', is => 'rw', required => 1, default => sub { 'apply' } |
48 | ); |
f670cfd0 |
49 | |
7adfd53f |
50 | has close_label => (isa => 'Str', is => 'rw', lazy_fail => 1); |
f670cfd0 |
51 | |
7adfd53f |
52 | has close_label_close => ( |
53 | isa => 'Str', is => 'rw', required => 1, default => sub { 'close' } |
54 | ); |
f670cfd0 |
55 | |
7adfd53f |
56 | has close_label_cancel => ( |
57 | isa => 'Str', is => 'rw', required => 1, default => sub { 'cancel' } |
58 | ); |
f670cfd0 |
59 | |
7adfd53f |
60 | sub fields { shift->_field_map } |
f670cfd0 |
61 | |
7adfd53f |
62 | implements BUILD => as { |
63 | my ($self, $args) = @_; |
64 | unless ($self->_has_field_map) { |
65 | my @field_map; |
66 | my $action = $self->action; |
67 | foreach my $attr ($action->parameter_attributes) { |
68 | push(@field_map, $self->build_fields_for($attr => $args)); |
69 | } |
f670cfd0 |
70 | |
7adfd53f |
71 | my %field_map = @field_map; |
72 | my @field_names = @{ $self->sort_by_spec( |
73 | $args->{column_order}, [keys %field_map] )}; |
f670cfd0 |
74 | |
7adfd53f |
75 | $self->_field_map(\%field_map); |
76 | $self->field_names(\@field_names); |
77 | } |
78 | $self->close_label($self->close_label_close); |
79 | }; |
f670cfd0 |
80 | |
7adfd53f |
81 | implements build_fields_for => as { |
82 | my ($self, $attr, $args) = @_; |
83 | my $attr_name = $attr->name; |
84 | #TODO: DOCUMENT ME!!!!!!!!!!!!!!!!! |
85 | my $builder = "build_fields_for_name_${attr_name}"; |
86 | my @fields; |
87 | if ($self->can($builder)) { |
88 | @fields = $self->$builder($attr, $args); # re-use coderef from can() |
89 | } elsif ($attr->has_type_constraint) { |
90 | my $constraint = $attr->type_constraint; |
91 | my $base_name = $constraint->name; |
92 | my $tried_isa = 0; |
93 | CONSTRAINT: while (defined($constraint)) { |
94 | my $name = $constraint->name; |
95 | if (eval { $name->can('meta') } && !$tried_isa++) { |
96 | foreach my $class ($name->meta->class_precedence_list) { |
97 | my $mangled_name = $class; |
98 | $mangled_name =~ s/:+/_/g; |
99 | my $builder = "build_fields_for_type_${mangled_name}"; |
100 | if ($self->can($builder)) { |
101 | @fields = $self->$builder($attr, $args); |
102 | last CONSTRAINT; |
103 | } |
104 | } |
105 | } |
106 | if (defined($name)) { |
107 | unless (defined($base_name)) { |
108 | $base_name = "(anon subtype of ${name})"; |
109 | } |
110 | my $mangled_name = $name; |
111 | $mangled_name =~ s/:+/_/g; |
112 | my $builder = "build_fields_for_type_${mangled_name}"; |
113 | if ($self->can($builder)) { |
114 | @fields = $self->$builder($attr, $args); |
115 | last CONSTRAINT; |
116 | } |
117 | } |
118 | $constraint = $constraint->parent; |
119 | } |
120 | if (!defined($constraint)) { |
121 | confess "Can't build field ${attr_name} of type ${base_name} without $builder method or build_fields_for_type_<type> method for type or any supertype"; |
122 | } |
123 | } else { |
124 | confess "Can't build field ${attr} without $builder method or type constraint"; |
125 | } |
126 | return @fields; |
127 | }; |
f670cfd0 |
128 | |
7adfd53f |
129 | implements build_field_map => as { |
130 | confess "Lazy field map building not supported by default"; |
131 | }; |
f670cfd0 |
132 | |
7adfd53f |
133 | implements can_apply => as { |
134 | my ($self) = @_; |
135 | foreach my $field (values %{$self->_field_map}) { |
136 | return 0 if $field->needs_sync; |
137 | # if e.g. a datetime field has an invalid value that can't be re-assembled |
138 | # into a datetime object, the action may be in a consistent state but |
139 | # not synchronized from the fields; in this case, we must not apply |
140 | } |
141 | return $self->action->can_apply; |
142 | }; |
f670cfd0 |
143 | |
7adfd53f |
144 | implements do_apply => as { |
145 | my $self = shift; |
146 | return $self->action->do_apply; |
147 | }; |
f670cfd0 |
148 | |
7adfd53f |
149 | implements ok => as { |
150 | my $self = shift; |
151 | if ($self->apply(@_)) { |
152 | $self->close(@_); |
153 | } |
154 | }; |
f670cfd0 |
155 | |
7adfd53f |
156 | implements apply => as { |
157 | my $self = shift; |
158 | if ($self->can_apply && (my $result = $self->do_apply)) { |
159 | $self->changed(0); |
160 | $self->close_label($self->close_label_close); |
f670cfd0 |
161 | $self->on_apply_callback->($self => $result) if $self->has_on_apply_callback; |
7adfd53f |
162 | return 1; |
163 | } else { |
164 | $self->changed(1); |
165 | $self->close_label($self->close_label_cancel); |
166 | return 0; |
167 | } |
168 | }; |
f670cfd0 |
169 | |
7adfd53f |
170 | implements close => as { |
171 | my $self = shift; |
172 | my ($controller, $name, @args) = @{$self->next_action}; |
173 | $controller->pop_viewport; |
174 | $controller->$name($self->action->ctx, @args); |
175 | }; |
f670cfd0 |
176 | |
7adfd53f |
177 | sub can_close { 1 } |
f670cfd0 |
178 | |
7adfd53f |
179 | override accept_events => sub { |
180 | (($_[0]->has_next_action ? ('ok', 'close') : ()), 'apply', super()); |
181 | }; # can't do a close-type operation if there's nowhere to go afterwards |
f670cfd0 |
182 | |
7adfd53f |
183 | override child_event_sinks => sub { |
184 | my ($self) = @_; |
185 | return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}), |
186 | (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}), |
187 | super()); |
188 | }; |
f670cfd0 |
189 | |
7adfd53f |
190 | after apply_child_events => sub { |
191 | # interrupt here because fields will have been updated |
192 | my ($self) = @_; |
193 | $self->sync_action_from_fields; |
194 | }; |
f670cfd0 |
195 | |
7adfd53f |
196 | implements sync_action_from_fields => as { |
197 | my ($self) = @_; |
198 | my $field_map = $self->_field_map; |
199 | my @fields = values %{$field_map}; |
200 | foreach my $field (@fields) { |
201 | $field->sync_to_action; # get the field to populate the $action if possible |
202 | } |
203 | $self->action->sync_all; |
204 | foreach my $field (@fields) { |
205 | $field->sync_from_action; # get errors from $action if applicable |
206 | } |
207 | }; |
f670cfd0 |
208 | |
7adfd53f |
209 | implements build_simple_field => as { |
210 | my ($self, $class, $attr, $args) = @_; |
211 | my $attr_name = $attr->name; |
212 | my %extra; |
213 | if (my $config = $args->{Field}{$attr_name}) { |
214 | %extra = %$config; |
215 | } |
216 | my $field = $class->new( |
217 | action => $self->action, |
218 | attribute => $attr, |
219 | name => $attr->name, |
f670cfd0 |
220 | location => join('-', $self->location, 'field', $attr->name), |
7adfd53f |
221 | ctx => $self->ctx, |
222 | %extra |
223 | ); |
224 | return ($attr_name => $field); |
225 | }; |
f670cfd0 |
226 | |
7adfd53f |
227 | implements build_fields_for_type_Num => as { |
228 | my ($self, $attr, $args) = @_; |
229 | return $self->build_simple_field(Number, $attr, $args); |
230 | }; |
f670cfd0 |
231 | |
7adfd53f |
232 | implements build_fields_for_type_Int => as { |
233 | my ($self, $attr, $args) = @_; |
234 | return $self->build_simple_field(Number, $attr, $args); |
235 | }; |
f670cfd0 |
236 | |
7adfd53f |
237 | implements build_fields_for_type_Bool => as { |
238 | my ($self, $attr, $args) = @_; |
239 | return $self->build_simple_field(Boolean, $attr, $args); |
240 | }; |
f670cfd0 |
241 | |
7adfd53f |
242 | implements build_fields_for_type_File => as { |
243 | my ($self, $attr, $args) = @_; |
244 | return $self->build_simple_field(File, $attr, $args); |
245 | }; |
f670cfd0 |
246 | |
7adfd53f |
247 | implements build_fields_for_type_Str => as { |
248 | my ($self, $attr, $args) = @_; |
249 | if ($attr->has_valid_values) { # There's probably a better way to do this |
250 | return $self->build_simple_field(ChooseOne, $attr, $args); |
251 | } |
252 | return $self->build_simple_field(Text, $attr, $args); |
253 | }; |
f670cfd0 |
254 | |
7adfd53f |
255 | implements build_fields_for_type_SimpleStr => as { |
256 | my ($self, $attr, $args) = @_; |
257 | return $self->build_simple_field(String, $attr, $args); |
258 | }; |
f670cfd0 |
259 | |
7adfd53f |
260 | implements build_fields_for_type_Password => as { |
261 | my ($self, $attr, $args) = @_; |
262 | return $self->build_simple_field(Password, $attr, $args); |
263 | }; |
f670cfd0 |
264 | |
7adfd53f |
265 | implements build_fields_for_type_DateTime => as { |
266 | my ($self, $attr, $args) = @_; |
267 | return $self->build_simple_field(DateTime, $attr, $args); |
268 | }; |
f670cfd0 |
269 | |
7adfd53f |
270 | implements build_fields_for_type_Enum => as { |
271 | my ($self, $attr, $args) = @_; |
272 | return $self->build_simple_field(ChooseOne, $attr, $args); |
273 | }; |
f670cfd0 |
274 | |
275 | #implements build_fields_for_type_Reaction_InterfaceModel_Object => as { |
7adfd53f |
276 | implements build_fields_for_type_DBIx_Class_Row => as { |
277 | my ($self, $attr, $args) = @_; |
278 | return $self->build_simple_field(ChooseOne, $attr, $args); |
279 | }; |
f670cfd0 |
280 | |
7adfd53f |
281 | implements build_fields_for_type_ArrayRef => as { |
282 | my ($self, $attr, $args) = @_; |
283 | if ($attr->has_valid_values) { |
284 | return $self->build_simple_field(ChooseMany, $attr, $args) |
285 | } else { |
286 | return $self->build_simple_field(HiddenArray, $attr, $args) |
287 | } |
288 | }; |
f670cfd0 |
289 | |
7adfd53f |
290 | implements build_fields_for_type_DateTime_Spanset => as { |
291 | my ($self, $attr, $args) = @_; |
292 | return $self->build_simple_field(TimeRange, $attr, $args); |
293 | }; |
f670cfd0 |
294 | |
7adfd53f |
295 | no Moose; |
f670cfd0 |
296 | |
7adfd53f |
297 | no strict 'refs'; |
298 | delete ${__PACKAGE__ . '::'}{inner}; |
299 | |
300 | }; |
301 | |
302 | 1; |
303 | |
304 | =head1 NAME |
305 | |
306 | Reaction::UI::ViewPort::ActionForm |
307 | |
308 | =head1 SYNOPSIS |
309 | |
310 | use aliased 'Reaction::UI::ViewPort::ActionForm'; |
311 | |
312 | $self->push_viewport(ActionForm, |
313 | layout => 'register', |
314 | action => $action, |
315 | next_action => [ $self, 'redirect_to', 'accounts', $c->req->captures ], |
316 | ctx => $c, |
317 | column_order => [ |
318 | qw / contact_title company_name email address1 address2 address3 |
319 | city country post_code telephone mobile fax/ ], |
320 | ); |
321 | |
322 | =head1 DESCRIPTION |
323 | |
324 | This subclass of viewport is used for rendering a collection of |
325 | L<Reaction::UI::ViewPort::Field> objects for user editing. |
326 | |
327 | =head1 ATTRIBUTES |
328 | |
329 | =head2 action |
330 | |
331 | L<Reaction::InterfaceModel::Action> |
332 | |
333 | =head2 ok_label |
334 | |
335 | Default: 'ok' |
336 | |
337 | =head2 apply_label |
338 | |
339 | Default: 'apply' |
340 | |
341 | =head2 close_label_close |
342 | |
343 | Default: 'close' |
344 | |
345 | =head2 close_label_cancel |
346 | |
347 | This label is only shown when C<changed> is true. |
348 | |
349 | Default: 'cancel' |
350 | |
351 | =head2 fields |
352 | |
353 | =head2 field_names |
354 | |
355 | Returns: Arrayref of field names. |
356 | |
357 | =head2 can_apply |
358 | |
359 | =head2 can_close |
360 | |
361 | =head2 changed |
362 | |
363 | Returns true if a field has been edited. |
364 | |
365 | =head2 next_action |
366 | |
367 | =head2 on_apply_callback |
368 | |
369 | CodeRef. |
370 | |
371 | =head1 METHODS |
372 | |
373 | =head2 ok |
374 | |
375 | Calls C<apply>, and then C<close> if successful. |
376 | |
377 | =head2 close |
378 | |
379 | Pop viewport and proceed to C<next_action>. |
380 | |
381 | =head2 apply |
382 | |
383 | Attempt to save changes and update C<changed> attribute if required. |
384 | |
385 | =head1 SEE ALSO |
386 | |
387 | L<Reaction::UI::ViewPort> |
388 | |
389 | L<Reaction::InterfaceModel::Action> |
390 | |
391 | =head1 AUTHORS |
392 | |
393 | See L<Reaction::Class> for authors. |
394 | |
395 | =head1 LICENSE |
396 | |
397 | See L<Reaction::Class> for the license. |
398 | |
399 | =cut |