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 => ( |
6ab43711 |
19 | isa => 'Reaction::InterfaceModel::Action', is => 'ro', required => 1 |
20 | ); |
f670cfd0 |
21 | |
9de685fc |
22 | has ordered_fields => (is => 'rw', isa => 'ArrayRef', lazy_build => 1); |
f670cfd0 |
23 | |
7adfd53f |
24 | has _field_map => ( |
6ab43711 |
25 | isa => 'HashRef', is => 'rw', init_arg => 'fields', lazy_build => 1, |
26 | ); |
f670cfd0 |
27 | |
7adfd53f |
28 | has changed => ( |
6ab43711 |
29 | isa => 'Int', is => 'rw', reader => 'is_changed', default => sub { 0 } |
30 | ); |
7adfd53f |
31 | |
32 | has next_action => ( |
6ab43711 |
33 | isa => 'ArrayRef', is => 'rw', required => 0, predicate => 'has_next_action' |
34 | ); |
f670cfd0 |
35 | |
7adfd53f |
36 | has on_apply_callback => ( |
6ab43711 |
37 | isa => 'CodeRef', is => 'rw', required => 0, |
38 | predicate => 'has_on_apply_callback' |
39 | ); |
f670cfd0 |
40 | |
7adfd53f |
41 | has ok_label => ( |
6ab43711 |
42 | isa => 'Str', is => 'rw', required => 1, default => sub { 'ok' } |
43 | ); |
f670cfd0 |
44 | |
7adfd53f |
45 | has apply_label => ( |
6ab43711 |
46 | isa => 'Str', is => 'rw', required => 1, default => sub { 'apply' } |
47 | ); |
f670cfd0 |
48 | |
7adfd53f |
49 | has close_label => (isa => 'Str', is => 'rw', lazy_fail => 1); |
f670cfd0 |
50 | |
7adfd53f |
51 | has close_label_close => ( |
6ab43711 |
52 | isa => 'Str', is => 'rw', required => 1, default => sub { 'close' } |
53 | ); |
f670cfd0 |
54 | |
7adfd53f |
55 | has close_label_cancel => ( |
6ab43711 |
56 | isa => 'Str', is => 'rw', required => 1, default => sub { 'cancel' } |
57 | ); |
f670cfd0 |
58 | |
7adfd53f |
59 | sub fields { shift->_field_map } |
f670cfd0 |
60 | |
7adfd53f |
61 | implements BUILD => as { |
62 | my ($self, $args) = @_; |
63 | unless ($self->_has_field_map) { |
64 | my @field_map; |
65 | my $action = $self->action; |
66 | foreach my $attr ($action->parameter_attributes) { |
89939ff9 |
67 | push(@field_map, $self->_build_fields_for($attr => $args)); |
7adfd53f |
68 | } |
9de685fc |
69 | $self->_field_map({ @field_map }); |
7adfd53f |
70 | } |
71 | $self->close_label($self->close_label_close); |
72 | }; |
f670cfd0 |
73 | |
89939ff9 |
74 | implements _build_fields_for => as { |
7adfd53f |
75 | my ($self, $attr, $args) = @_; |
76 | my $attr_name = $attr->name; |
77 | #TODO: DOCUMENT ME!!!!!!!!!!!!!!!!! |
89939ff9 |
78 | my $builder = "_build_fields_for_name_${attr_name}"; |
7adfd53f |
79 | my @fields; |
80 | if ($self->can($builder)) { |
81 | @fields = $self->$builder($attr, $args); # re-use coderef from can() |
82 | } elsif ($attr->has_type_constraint) { |
83 | my $constraint = $attr->type_constraint; |
84 | my $base_name = $constraint->name; |
85 | my $tried_isa = 0; |
6ab43711 |
86 | CONSTRAINT: while (defined($constraint)) { |
7adfd53f |
87 | my $name = $constraint->name; |
de48f4e6 |
88 | $name = $attr->_isa_metadata if($name eq '__ANON__'); |
7adfd53f |
89 | if (eval { $name->can('meta') } && !$tried_isa++) { |
90 | foreach my $class ($name->meta->class_precedence_list) { |
91 | my $mangled_name = $class; |
92 | $mangled_name =~ s/:+/_/g; |
89939ff9 |
93 | my $builder = "_build_fields_for_type_${mangled_name}"; |
7adfd53f |
94 | if ($self->can($builder)) { |
95 | @fields = $self->$builder($attr, $args); |
96 | last CONSTRAINT; |
97 | } |
98 | } |
99 | } |
100 | if (defined($name)) { |
101 | unless (defined($base_name)) { |
102 | $base_name = "(anon subtype of ${name})"; |
103 | } |
104 | my $mangled_name = $name; |
105 | $mangled_name =~ s/:+/_/g; |
89939ff9 |
106 | my $builder = "_build_fields_for_type_${mangled_name}"; |
7adfd53f |
107 | if ($self->can($builder)) { |
108 | @fields = $self->$builder($attr, $args); |
109 | last CONSTRAINT; |
110 | } |
111 | } |
112 | $constraint = $constraint->parent; |
113 | } |
114 | if (!defined($constraint)) { |
89939ff9 |
115 | 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"; |
7adfd53f |
116 | } |
117 | } else { |
118 | confess "Can't build field ${attr} without $builder method or type constraint"; |
119 | } |
120 | return @fields; |
121 | }; |
f670cfd0 |
122 | |
89939ff9 |
123 | implements _build_field_map => as { |
7adfd53f |
124 | confess "Lazy field map building not supported by default"; |
125 | }; |
f670cfd0 |
126 | |
89939ff9 |
127 | implements _build_ordered_fields => as { |
9de685fc |
128 | my $self = shift; |
6ab43711 |
129 | my $ordered = $self->sort_by_spec($self->column_order, [keys %{$self->_field_map}]); |
130 | return [@{$self->_field_map}{@$ordered}]; |
9de685fc |
131 | }; |
132 | |
7adfd53f |
133 | implements can_apply => as { |
134 | my ($self) = @_; |
6ab43711 |
135 | foreach my $field ( @{ $self->ordered_fields } ) { |
7adfd53f |
136 | return 0 if $field->needs_sync; |
6ab43711 |
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 |
7adfd53f |
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 | |
89939ff9 |
209 | implements _build_simple_field => as { |
7adfd53f |
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( |
6ab43711 |
217 | action => $self->action, |
218 | attribute => $attr, |
219 | name => $attr->name, |
220 | location => join('-', $self->location, 'field', $attr->name), |
221 | ctx => $self->ctx, |
222 | %extra |
223 | ); |
7adfd53f |
224 | return ($attr_name => $field); |
225 | }; |
f670cfd0 |
226 | |
89939ff9 |
227 | implements _build_fields_for_type_Num => as { |
7adfd53f |
228 | my ($self, $attr, $args) = @_; |
89939ff9 |
229 | return $self->_build_simple_field(Number, $attr, $args); |
7adfd53f |
230 | }; |
f670cfd0 |
231 | |
89939ff9 |
232 | implements _build_fields_for_type_Int => as { |
7adfd53f |
233 | my ($self, $attr, $args) = @_; |
89939ff9 |
234 | return $self->_build_simple_field(Number, $attr, $args); |
7adfd53f |
235 | }; |
f670cfd0 |
236 | |
89939ff9 |
237 | implements _build_fields_for_type_Bool => as { |
7adfd53f |
238 | my ($self, $attr, $args) = @_; |
89939ff9 |
239 | return $self->_build_simple_field(Boolean, $attr, $args); |
7adfd53f |
240 | }; |
f670cfd0 |
241 | |
89939ff9 |
242 | implements _build_fields_for_type_File => as { |
7adfd53f |
243 | my ($self, $attr, $args) = @_; |
89939ff9 |
244 | return $self->_build_simple_field(File, $attr, $args); |
7adfd53f |
245 | }; |
f670cfd0 |
246 | |
89939ff9 |
247 | implements _build_fields_for_type_Str => as { |
7adfd53f |
248 | my ($self, $attr, $args) = @_; |
249 | if ($attr->has_valid_values) { # There's probably a better way to do this |
89939ff9 |
250 | return $self->_build_simple_field(ChooseOne, $attr, $args); |
7adfd53f |
251 | } |
89939ff9 |
252 | return $self->_build_simple_field(Text, $attr, $args); |
7adfd53f |
253 | }; |
f670cfd0 |
254 | |
89939ff9 |
255 | implements _build_fields_for_type_SimpleStr => as { |
7adfd53f |
256 | my ($self, $attr, $args) = @_; |
89939ff9 |
257 | return $self->_build_simple_field(String, $attr, $args); |
7adfd53f |
258 | }; |
f670cfd0 |
259 | |
89939ff9 |
260 | implements _build_fields_for_type_Password => as { |
7adfd53f |
261 | my ($self, $attr, $args) = @_; |
89939ff9 |
262 | return $self->_build_simple_field(Password, $attr, $args); |
7adfd53f |
263 | }; |
f670cfd0 |
264 | |
89939ff9 |
265 | implements _build_fields_for_type_DateTime => as { |
7adfd53f |
266 | my ($self, $attr, $args) = @_; |
89939ff9 |
267 | return $self->_build_simple_field(DateTime, $attr, $args); |
7adfd53f |
268 | }; |
f670cfd0 |
269 | |
89939ff9 |
270 | implements _build_fields_for_type_Enum => as { |
7adfd53f |
271 | my ($self, $attr, $args) = @_; |
89939ff9 |
272 | return $self->_build_simple_field(ChooseOne, $attr, $args); |
7adfd53f |
273 | }; |
f670cfd0 |
274 | |
275 | #implements build_fields_for_type_Reaction_InterfaceModel_Object => as { |
89939ff9 |
276 | implements _build_fields_for_type_DBIx_Class_Row => as { |
7adfd53f |
277 | my ($self, $attr, $args) = @_; |
89939ff9 |
278 | return $self->_build_simple_field(ChooseOne, $attr, $args); |
7adfd53f |
279 | }; |
f670cfd0 |
280 | |
89939ff9 |
281 | implements _build_fields_for_type_ArrayRef => as { |
7adfd53f |
282 | my ($self, $attr, $args) = @_; |
283 | if ($attr->has_valid_values) { |
89939ff9 |
284 | return $self->_build_simple_field(ChooseMany, $attr, $args) |
7adfd53f |
285 | } else { |
89939ff9 |
286 | return $self->_build_simple_field(HiddenArray, $attr, $args) |
7adfd53f |
287 | } |
288 | }; |
f670cfd0 |
289 | |
89939ff9 |
290 | implements _build_fields_for_type_DateTime_Spanset => as { |
7adfd53f |
291 | my ($self, $attr, $args) = @_; |
89939ff9 |
292 | return $self->_build_simple_field(TimeRange, $attr, $args); |
7adfd53f |
293 | }; |
f670cfd0 |
294 | |
7adfd53f |
295 | no Moose; |
f670cfd0 |
296 | |
7adfd53f |
297 | no strict 'refs'; |
298 | delete ${__PACKAGE__ . '::'}{inner}; |
299 | |
300 | }; |
301 | |
6ab43711 |
302 | 1; |
7adfd53f |
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 | |
7adfd53f |
353 | =head2 can_apply |
354 | |
355 | =head2 can_close |
356 | |
357 | =head2 changed |
358 | |
359 | Returns true if a field has been edited. |
360 | |
361 | =head2 next_action |
362 | |
363 | =head2 on_apply_callback |
364 | |
365 | CodeRef. |
366 | |
367 | =head1 METHODS |
368 | |
369 | =head2 ok |
370 | |
371 | Calls C<apply>, and then C<close> if successful. |
372 | |
373 | =head2 close |
374 | |
375 | Pop viewport and proceed to C<next_action>. |
376 | |
377 | =head2 apply |
378 | |
379 | Attempt to save changes and update C<changed> attribute if required. |
380 | |
381 | =head1 SEE ALSO |
382 | |
383 | L<Reaction::UI::ViewPort> |
384 | |
385 | L<Reaction::InterfaceModel::Action> |
386 | |
387 | =head1 AUTHORS |
388 | |
389 | See L<Reaction::Class> for authors. |
390 | |
391 | =head1 LICENSE |
392 | |
393 | See L<Reaction::Class> for the license. |
394 | |
395 | =cut |