File Coverage

blib/lib/RapidApp/Module/DbicCombo.pm
Criterion Covered Total %
statement 21 66 31.8
branch 4 34 11.7
condition 0 15 0.0
subroutine 6 13 46.1
pod 0 2 0.0
total 31 130 23.8


line stmt bran cond sub pod time code
1             package RapidApp::Module::DbicCombo;
2              
3 5     5   30 use strict;
  5         11  
  5         148  
4 5     5   23 use warnings;
  5         9  
  5         133  
5              
6 5     5   102 use Moose;
  5         10  
  5         36  
7             extends 'RapidApp::Module::Combo';
8              
9 5     5   28508 use RapidApp::Util qw(:all);
  5         12  
  5         2161  
10 5     5   35 use List::Util;
  5         8  
  5         5738  
11              
12             has 'ResultSet' => ( is => 'ro', isa => 'Object', required => 1 );
13             has 'RS_condition' => ( is => 'ro', isa => 'Ref', default => sub {{}} );
14             has 'RS_attr' => ( is => 'ro', isa => 'Ref', default => sub {{}} );
15             has 'record_pk' => ( is => 'ro', isa => 'Str', required => 1 );
16              
17             # We don't need datastore-plus because we're not a CRUD interface, etc...
18             # the standard Ext.data.JsonStore API is all we need:
19             has '+no_datastore_plus_plugin', default => 1;
20              
21             =head2 user_editable
22              
23             Boolean. If true, the combo field will allow the user to enter arbitrary text in
24             addition to selecting an existing item from the list. Defaults to false.
25             =cut
26             has 'user_editable', is => 'ro', isa => 'Bool', default => sub{0};
27              
28             =head2 type_filter
29              
30             Boolean. If true, the combo field will allow the user to type in arbitrary text to filter the list
31             of results. Defaults to false unless user_editable is true.
32             =cut
33             has 'type_filter', is => 'ro', isa => 'Bool', lazy => 1, default => sub{ (shift)->user_editable };
34              
35             =head2 min_type_filter_chars
36              
37             Setting passed to the 'minChars' ExtJS combo config. Defaults to '0' which causes filter/query
38             to fire with every user ketstroke. For large tables where matching on as little as a single character
39             will be too slow, or to reduce the number/rate of queries fired, set this to a higher value.
40             =cut
41             has 'min_type_filter_chars', is => 'ro', isa => 'Int', default => 0;
42              
43             =head2 auto_complete
44              
45             Boolean. True to enable 'typeAhead' in the ExtJS combo, meaning that text from the filtered results
46             will be auto-completed in the box. This only makes sense when type_filter is on and will auto enable
47             filter_match_start. This is an "unfeature" (very annoying) if used inappropriately. Defaults to false.
48             =cut
49             has 'auto_complete', is => 'ro', isa => 'Bool', default => sub{0};
50              
51              
52             =head2 filter_match_start
53              
54             Boolean. True for the LIKE query generated for the type_filter to match only the start of the display
55             column. This defaults to true if auto_complete is enabled because it wouldn't make sense otherwise.
56             =cut
57             has 'filter_match_start', is => 'ro', isa => 'Bool', lazy => 1, default => sub{ (shift)->auto_complete };
58              
59              
60             has 'result_class', is => 'ro', lazy => 1, default => sub {
61             my $self = shift;
62             my $Source = $self->ResultSet->result_source;
63             $Source->schema->class($Source->source_name);
64             }, isa => 'Str', init_arg => undef;
65              
66             sub BUILD {
67 6     6 0 110 my $self = shift;
68            
69             # Remove the width hard coded in AppCombo2 (still being left in AppCombo2 for legacy
70             # but will be removed in the future)
71 6         218 $self->delete_extconfig_param('width');
72            
73             # base config:
74 6         184 $self->apply_extconfig(
75             itemId => $self->name . '_combo',
76             forceSelection => \1,
77             editable => \0,
78             typeAhead => \0,
79             selectOnFocus => \0,
80             );
81            
82             # type_filter overrides:
83 6 100       200 $self->apply_extconfig(
84             editable => \1,
85             queryParam => 'type_filter_query',
86             minChars => $self->min_type_filter_chars,
87             emptyText => 'Type to Find',
88             emptyClass => 'field-empty-text',
89             listEmptyText => join("\n",
90             '<div style="padding:5px;" class="field-empty-text">',
91             '(No matches found)',
92             '</div>'
93             ),
94             ) if ($self->type_filter);
95            
96             # auto_complete overrides:
97 6 50       180 $self->apply_extconfig(
98             typeAhead => \1
99             ) if ($self->auto_complete);
100            
101             # user_editable overrides:
102 6 50       158 $self->apply_extconfig(
103             editable => \1,
104             forceSelection => \0,
105             emptyText => undef,
106             listEmptyText => '',
107             triggerClass => 'x-form-search-trigger',
108             no_click_trigger => \1,
109             is_user_editable => \1,
110             autoSelect => \0
111             ) if ($self->user_editable);
112             }
113              
114              
115             sub read_records {
116 0     0 0   my $self = shift;
117 0           my $p = $self->c->req->params;
118            
119             delete $p->{type_filter_query} if ( $p->{type_filter_query} && (
120             # Discard type_filter queries if type_filter is not enabled:
121             ! $self->type_filter ||
122             # As well as empty/only whitespace
123 0 0 0       $p->{type_filter_query} =~ /^\s*$/
      0        
124             ));
125            
126             # record_pk and valueField are almost always the the same
127 0           my @cols = uniq(
128             $self->record_pk,
129             $self->valueField,
130             $self->displayField
131             );
132            
133             # Start with a select on only the columns we need:
134             my $Rs = $self->ResultSet->search_rs(undef,{
135 0           select => [map {$self->_resolve_select($_)} @cols],
  0            
136             as => \@cols
137             });
138              
139             # Then apply the optional RS_condition/RS_attr
140 0           $Rs = $Rs->search_rs(
141             $self->RS_condition,
142             $self->RS_attr
143             );
144            
145             # Set the default order_by so the list is sorted alphabetically:
146             $Rs = $Rs->search_rs(undef,{
147             order_by => {
148             '-asc' => $self->_resolve_select($self->displayField)
149             }
150 0 0         }) unless (exists $Rs->{attrs}{order_by});
151            
152             # And set a fail-safe max number of rows:
153 0 0         $Rs = $Rs->search_rs(undef,{ rows => 500 }) unless (exists $Rs->{attrs}{rows});
154            
155             # Filter for type_filter
156             $Rs = $Rs->search_rs(
157             $self->_like_type_filter_for($p->{type_filter_query})
158 0 0         ) if ($p->{type_filter_query});
159            
160             # Finally, chain through the custom 'AppComboRs' ResultSet method if defined:
161 0 0         $Rs = $Rs->AppComboRs if ($Rs->can('AppComboRs'));
162            
163 0           my $rows = [ $Rs
164             ->search_rs(undef, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' })
165             ->all
166             ];
167              
168             # Sort results into groups according to the kind of match (#83)
169             $rows = $self->_apply_type_filter_row_order(
170             $rows,$p->{type_filter_query}
171 0 0         ) if ($p->{type_filter_query});
172              
173             # Handle the 'valueqry' separately because it supercedes the rest of the
174             # query, and applies to only 1 row. However, we don't expect both a valueqry
175             # and a type_filter_query together. The valueqry is sent to obtain the display
176             # value for an existing value in the combo, and we support the case of
177             # properly displaying an existing value even if it does not show up (i.e.
178             # cannot be selected) in the dropdown list.
179             $self->_apply_valueqry($rows,$p->{valueqry}) if (
180             $p->{valueqry} &&
181             ! $p->{type_filter_query}
182 0 0 0       );
183              
184             return {
185 0           rows => $rows,
186             results => scalar(@$rows)
187             };
188             }
189              
190              
191             sub _apply_valueqry {
192 0     0     my ($self, $rows, $valueqry) = @_;
193            
194             # If the valueqry row is already present, we don't need to do anything:
195             return if ( List::Util::first {
196 0     0     $_->{$self->record_pk} eq $valueqry
197 0 0         } @$rows );
198            
199 0 0         my $Row = $self->ResultSet
200             ->search_rs({ join('.','me',$self->record_pk) => { '=' => $valueqry }})
201             ->first
202             or return;
203            
204 0           unshift @$rows, { $Row->get_columns };
205             }
206              
207             sub _resolve_select {
208 0     0     my ($self, $col) = @_;
209            
210 0   0       $self->{_resolve_select_cache}{$col} ||= do {
211 0           my $Source = $self->ResultSet->result_source;
212 0           my $class = $Source->schema->class($Source->source_name);
213 0 0 0       $class->can('has_virtual_column') && $class->has_virtual_column($col)
214             ? $class->_virtual_column_select($col)
215             : join('.','me',$col)
216             };
217             }
218              
219             require RapidApp::Module::StorCmp::Role::DbicLnk;
220 0     0     sub _binary_op_fuser { RapidApp::Module::StorCmp::Role::DbicLnk::_binary_op_fuser(@_) }
221              
222             sub _like_type_filter_for {
223 0     0     my ($self,$str) = @_;
224            
225 0 0         my $like_arg = $self->filter_match_start
226             ? join('', $str,'%') # <-- start of the column
227             : join('','%',$str,'%'); # <-- anywhere in the column
228            
229 0           my $sel = $self->_resolve_select($self->displayField);
230            
231 0           my $sm = $self->ResultSet->result_source->schema->storage->sql_maker;
232 0           return &_binary_op_fuser($sm,$sel => { like => $like_arg });
233              
234             # Manual alternative to _binary_op_fuser above:
235             #$sel = $$sel if (ref $sel);
236             #return \[join(' ',$sel,'LIKE ?'),$like_arg];
237             }
238              
239              
240             # Orders the rows by the kind of match: exact, start, then everything else (#83)
241             sub _apply_type_filter_row_order {
242 0     0     my ($self, $rows, $str) = @_;
243              
244 0           my @exact = ();
245 0           my @start = ();
246 0           my @other = ();
247              
248 0           $str = lc($str); # case-insensitive
249              
250 0           for my $row (@$rows) {
251 0 0         exists $row->{$self->displayField} or die "Bad data -- row doesn't contain displayField key";
252              
253 0           my $dval = lc($row->{$self->displayField}); # case-insensitive
254              
255 0 0         if ($dval eq $str) {
    0          
256 0           push @exact, $row;
257             }
258             elsif ($dval =~ /^$str/) {
259 0           push @start, $row;
260             }
261             else {
262 0           push @other, $row;
263             }
264             }
265              
266 0           @$rows = (@exact,@start,@other);
267              
268 0           $rows
269             }
270              
271              
272             1;
273              
274