Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision fac8417d

Von Moritz Bunkus vor mehr als 9 Jahren hinzugefügt

  • ID fac8417d136f89b1ce0fed8c2ef35ee089228ac7
  • Vorgänger d41efcfe
  • Nachfolger e943a04e

Project-Picker basierend auf Part-Picker

Unterschiede anzeigen:

SL/Controller/Project.pm
23 23
use SL::Locale::String;
24 24

  
25 25
use Data::Dumper;
26
use JSON;
27
use Rose::DB::Object::Helpers qw(as_tree);
26 28

  
27 29
use Rose::Object::MakeMethods::Generic
28 30
(
29 31
 scalar => [ qw(project linked_records) ],
30
 'scalar --get_set_init' => [ qw(models customers project_types project_statuses) ],
32
 'scalar --get_set_init' => [ qw(models customers project_types project_statuses projects) ],
31 33
);
32 34

  
33
__PACKAGE__->run_before('check_auth');
34
__PACKAGE__->run_before('load_project',        only => [ qw(edit update destroy) ]);
35
__PACKAGE__->run_before('check_auth',   except => [ qw(ajax_autocomplete) ]);
36
__PACKAGE__->run_before('load_project', only   => [ qw(edit update destroy) ]);
35 37

  
36 38
#
37 39
# actions
......
102 104
  $self->redirect_to(action => 'search');
103 105
}
104 106

  
107
sub action_ajax_autocomplete {
108
  my ($self, %params) = @_;
109

  
110
  $::form->{filter}{'all:substr:multi::ilike'} =~ s{[\(\)]+}{}g;
111

  
112
  # if someone types something, and hits enter, assume he entered the full name.
113
  # if something matches, treat that as sole match
114
  # unfortunately get_models can't do more than one per package atm, so we d it
115
  # the oldfashioned way.
116
  if ($::form->{prefer_exact}) {
117
    my $exact_matches;
118
    if (1 == scalar @{ $exact_matches = SL::DB::Manager::Project->get_all(
119
      query => [
120
        obsolete => 0,
121
        SL::DB::Manager::Project->type_filter($::form->{filter}{type}),
122
        or => [
123
          description   => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
124
          projectnumber => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
125
        ]
126
      ],
127
      limit => 2,
128
    ) }) {
129
      $self->projects($exact_matches);
130
    }
131
  }
132

  
133
  $::form->{sort_by} = 'customer_and_description';
134

  
135
  my @hashes = map {
136
   +{
137
     value         => $_->full_description(style => 'full'),
138
     label         => $_->full_description(style => 'full'),
139
     id            => $_->id,
140
     projectnumber => $_->projectnumber,
141
     description   => $_->description,
142
     cvars         => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } },
143
    }
144
  } @{ $self->projects }; # neato: if exact match triggers we don't even need the init_projects
145

  
146
  $self->render(\ SL::JSON::to_json(\@hashes), { layout => 0, type => 'json', process => 0 });
147
}
148

  
149
sub action_test_page {
150
  $_[0]->render('project/test_page');
151
}
152

  
105 153
#
106 154
# filters
107 155
#
......
117 165
sub init_project_statuses { SL::DB::Manager::ProjectStatus->get_all_sorted }
118 166
sub init_project_types    { SL::DB::Manager::ProjectType->get_all_sorted   }
119 167

  
168
sub init_projects {
169
  if ($::form->{no_paginate}) {
170
    $_[0]->models->disable_plugin('paginated');
171
  }
172

  
173
  $_[0]->models->get;
174
}
175

  
120 176
sub init_customers {
121 177
  my ($self)      = @_;
122 178
  my @customer_id = $self->project && $self->project->customer_id ? (id => $self->project->customer_id) : ();
......
251 307
      projectnumber  => t8('Project Number'),
252 308
      project_type   => t8('Project Type'),
253 309
      project_status => t8('Project Status'),
310
      customer_and_description => 1,
254 311
    },
255 312
    with_objects => [ 'customer', 'project_status', 'project_type' ],
256 313
  );
SL/DB/Manager/Project.pm
30 30
    return () if $value ne 'orphaned';
31 31
    return __PACKAGE__->is_not_used_filter($prefix);
32 32
  },
33
  all => sub {
34
    my ($key, $value, $prefix) = @_;
35
    return or => [ map { $prefix . $_ => $value } qw(projectnumber description customer.name) ]
36
  }
33 37
);
34 38

  
35 39
our %project_id_column_prefixes = (
......
49 53
      customer     => 'customer.name',
50 54
      project_type => 'project_type.description',
51 55
      project_status => 'project_status.description',
56
      customer_and_description => [ qw(customer.name project.description) ],
52 57
    });
53 58
}
54 59

  
SL/DB/Project.pm
60 60
  } elsif ($params{style} =~ m/description/) {
61 61
    $description = $self->description;
62 62

  
63
  } elsif ($params{style} =~ m/full/) {
64
    $description = $self->projectnumber;
65
    if ($self->description && do { my $desc = quotemeta $self->description; $self->projectnumber !~ m/$desc/ }) {
66
      $description .= ' ' . $self->description;
67
    }
68

  
69
    $description = $self->customer->name . " (${description})";
70

  
63 71
  } else {
64 72
    $description = $self->projectnumber;
65 73
    if ($self->description && do { my $desc = quotemeta $self->description; $self->projectnumber !~ m/$desc/ }) {
......
128 136

  
129 137
Returns only the project's description.
130 138

  
139
=item C<full>
140

  
141
Returns the customer name followed by the project number and project
142
description in parenthesis (e.g. "Evil Corp (12345 World
143
domination)"). If the project's description is already part of the
144
project's number then it will not be appended.
145

  
131 146
=back
132 147

  
133 148
=back
SL/Presenter/Project.pm
5 5
use parent qw(Exporter);
6 6

  
7 7
use Exporter qw(import);
8
our @EXPORT = qw(project);
8
our @EXPORT = qw(project project_picker);
9 9

  
10 10
use Carp;
11 11

  
......
29 29
  return $self->escaped_text($text);
30 30
}
31 31

  
32
sub project_picker {
33
  my ($self, $name, $value, %params) = @_;
34

  
35
  $value      = SL::DB::Manager::Project->find_by(id => $value) if $value && !ref $value;
36
  my $id      = delete($params{id}) || $self->name_to_id($name);
37
  my @classes = $params{class} ? ($params{class}) : ();
38
  push @classes, 'project_autocomplete';
39

  
40
  my $ret =
41
    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
42
    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(customer_id)) .
43
    $self->input_tag("", ref $value ? $value->displayable_name : '', id => "${id}_name", %params);
44

  
45
  $::request->layout->add_javascripts('autocomplete_project.js');
46
  $::request->presenter->need_reinit_widgets($id);
47

  
48
  $self->html_tag('span', $ret, class => 'project_picker');
49
}
50

  
32 51
1;
33 52

  
34 53
__END__
SL/Template/Plugin/L.pm
69 69
sub part_picker   { return _call_presenter('part_picker',   @_); }
70 70
sub chart_picker  { return _call_presenter('chart_picker',  @_); }
71 71
sub customer_vendor_picker   { return _call_presenter('customer_vendor_picker',   @_); }
72
sub project_picker           { return _call_presenter('project_picker',           @_); }
72 73

  
73 74
sub _set_id_attribute {
74 75
  my ($attributes, $name, $unique) = @_;
css/kivitendo/main.css
394 394
}
395 395
.customer-vendor-picker-undefined,
396 396
.chartpicker-undefined,
397
.projectpicker-undefined,
397 398
.partpicker-undefined {
398 399
  color: red;
399 400
  font-style: italic;
js/autocomplete_project.js
1
namespace('kivi', function(k){
2
  k.ProjectPicker = function($real, options) {
3
    // short circuit in case someone double inits us
4
    if ($real.data("project_picker"))
5
      return $real.data("project_picker");
6

  
7
    var KEY = {
8
      ESCAPE: 27,
9
      ENTER:  13,
10
      TAB:    9,
11
      LEFT:   37,
12
      RIGHT:  39,
13
      PAGE_UP: 33,
14
      PAGE_DOWN: 34,
15
    };
16
    var CLASSES = {
17
      PICKED:       'projectpicker-picked',
18
      UNDEFINED:    'projectpicker-undefined',
19
    }
20
    var o = $.extend({
21
      limit: 20,
22
      delay: 50,
23
    }, options);
24
    var STATES = {
25
      PICKED:    CLASSES.PICKED,
26
      UNDEFINED: CLASSES.UNDEFINED
27
    }
28
    var real_id      = $real.attr('id');
29
    var $dummy       = $('#' + real_id + '_name');
30
    var $customer_id = $('#' + real_id + '_customer_id');
31
    var state        = STATES.PICKED;
32
    var last_real    = $real.val();
33
    var last_dummy   = $dummy.val();
34
    var timer;
35

  
36
    function ajax_data(term) {
37
      var data = {
38
        'filter.all:substr:multi::ilike': term,
39
        'filter.valid': 'valid',
40
        no_paginate:  $('#no_paginate').prop('checked') ? 1 : 0,
41
        current:  $real.val(),
42
      };
43

  
44
      if ($customer_id && $customer_id.val())
45
        data['filter.customer_id'] = $customer_id.val().split(',');
46

  
47
      return data;
48
    }
49

  
50
    function set_item (item) {
51
      if (item.id) {
52
        $real.val(item.id);
53
        // autocomplete ui has name, use the value for ajax items, which contains displayable_name
54
        $dummy.val(item.name ? item.name : item.value);
55
      } else {
56
        $real.val('');
57
        $dummy.val('');
58
      }
59
      state                 = STATES.PICKED;
60
      last_real             = $real.val();
61
      last_dummy            = $dummy.val();
62
      last_unverified_dummy = $dummy.val();
63

  
64
      $real.trigger('change');
65
      $real.trigger('set_item:ProjectPicker', item);
66

  
67
      annotate_state();
68
    }
69

  
70
    function make_defined_state () {
71
      if (state == STATES.PICKED) {
72
        annotate_state();
73
        return true
74
      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
75
        set_item({})
76
      else {
77
        last_unverified_dummy = $dummy.val();
78
        set_item({ id: last_real, name: last_dummy })
79
      }
80
      annotate_state();
81
    }
82

  
83
    function annotate_state () {
84
      if (state == STATES.PICKED)
85
        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
86
      else if (state == STATES.UNDEFINED && $dummy.val() == '')
87
        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
88
      else {
89
        last_unverified_dummy = $dummy.val();
90
        $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
91
      }
92
    }
93

  
94
    function update_results () {
95
      $.ajax({
96
        url: 'controller.pl?action=Project/project_picker_result',
97
        data: $.extend({
98
            'real_id': $real.val(),
99
        }, ajax_data(function(){ var val = $('#project_picker_filter').val(); return val === undefined ? '' : val })),
100
        success: function(data){ $('#project_picker_result').html(data) }
101
      });
102
    };
103

  
104
    function result_timer (event) {
105
      if (!$('no_paginate').prop('checked')) {
106
        if (event.keyCode == KEY.PAGE_UP) {
107
          $('#project_picker_result a.paginate-prev').click();
108
          return;
109
        }
110
        if (event.keyCode == KEY.PAGE_DOWN) {
111
          $('#project_picker_result a.paginate-next').click();
112
          return;
113
        }
114
      }
115
      window.clearTimeout(timer);
116
      timer = window.setTimeout(update_results, 100);
117
    }
118

  
119
    $dummy.autocomplete({
120
      source: function(req, rsp) {
121
        $.ajax($.extend(o, {
122
          url:      'controller.pl?action=Project/ajax_autocomplete',
123
          dataType: "json",
124
          data:     ajax_data(req.term),
125
          success:  function (data){ rsp(data) }
126
        }));
127
      },
128
      select: function(event, ui) {
129
        set_item(ui.item);
130
      },
131
    });
132
    /*  In case users are impatient and want to skip ahead:
133
     *  Capture <enter> key events and check if it's a unique hit.
134
     *  If it is, go ahead and assume it was selected. If it wasn't don't do
135
     *  anything so that autocompletion kicks in.  For <tab> don't prevent
136
     *  propagation. It would be nice to catch it, but javascript is too stupid
137
     *  to fire a tab event later on, so we'd have to reimplement the "find
138
     *  next active element in tabindex order and focus it".
139
     */
140
    /* note:
141
     *  event.which does not contain tab events in keypressed in firefox but will report 0
142
     *  chrome does not fire keypressed at all on tab or escape
143
     */
144
    $dummy.keydown(function(event){
145
      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
146
        // if string is empty assume they want to delete
147
        if ($dummy.val() == '') {
148
          set_item({});
149
          return true;
150
        } else if (state == STATES.PICKED) {
151
          return true;
152
        }
153
        if (event.which == KEY.TAB) event.preventDefault();
154
        $.ajax({
155
          url: 'controller.pl?action=Project/ajax_autocomplete',
156
          dataType: "json",
157
          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
158
          success: function (data) {
159
            if (data.length == 1) {
160
              set_item(data[0]);
161
              if (event.which == KEY.ENTER)
162
                $('#update_button').click();
163
            } else {
164
            }
165
            annotate_state();
166
          }
167
        });
168
        if (event.which == KEY.ENTER)
169
          return false;
170
      } else {
171
        state = STATES.UNDEFINED;
172
      }
173
    });
174

  
175
    $dummy.blur(function(){
176
      window.clearTimeout(timer);
177
      timer = window.setTimeout(annotate_state, 100);
178
    });
179

  
180
    // now add a picker div after the original input
181
    var pcont  = $('<span>').addClass('position-absolute');
182
    var picker = $('<div>');
183
    $dummy.after(pcont);
184
    pcont.append(picker);
185

  
186
    var pp = {
187
      real:           function() { return $real },
188
      dummy:          function() { return $dummy },
189
      type:           function() { return $type },
190
      customer_id:    function() { return $customer_id },
191
      update_results: update_results,
192
      result_timer:   result_timer,
193
      set_item:       set_item,
194
      reset:          make_defined_state,
195
      is_defined_state: function() { return state == STATES.PICKED },
196
      init_results:    function () {
197
        $('div.project_picker_project').each(function(){
198
          $(this).click(function(){
199
            set_item({
200
              id:   $(this).children('input.project_picker_id').val(),
201
              name: $(this).children('input.project_picker_description').val(),
202
            });
203
            $dummy.focus();
204
            return true;
205
          });
206
        });
207
        $('#project_selection').keydown(function(e){
208
           if (e.which == KEY.ESCAPE) {
209
             $dummy.focus();
210
           }
211
        });
212
      }
213
    }
214
    $real.data('project_picker', pp);
215
    return pp;
216
  }
217
});
218

  
219
$(function(){
220
  $('input.project_autocomplete').each(function(i,real){
221
    kivi.ProjectPicker($(real));
222
  })
223
});
templates/webpages/project/test_page.html
1
[% USE L %]
2

  
3
<h1>Projekt-Picker-Testpage</h1>
4

  
5
<br>
6
[% L.project_picker('project_id', '', style='width: 600px') %] text<br>

Auch abrufbar als: Unified diff