The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
    // the column model has information about grid columns.
    // dataIndex maps the column to the specific data field in
    // the data store

    // returns a little icon which, when clicked on, redirects the browser to
    // the grid of a related table
    function createGridButton(id, table, url, pk) {
        new Ext.Button({
            cls: 'x-btn-text-icon'
            ,tooltip: 'Jump to this ' + table
            ,handler: function(btn, e) {
                Ext.DomHelper.append(
                    Ext.getBody()
                    ,{
                        tag: 'form'
                        ,name: 'transportform'
                        ,id: 'transportform'
                        ,action: url
                        ,method: 'POST'
                        ,cn: pk
                    }
                ).submit();
            }
            ,icon: '[% c.uri_for( c.controller('AutoCRUD::Static').action_for('cpacstatic'),
                                            "table_go.png" ) %]'
        }).render(document.body, id);
    }

    // text added to FK combo list to hint for the full-text-search option
    var fk_combo_comment = ' (all matches)';

    // create reusable renderer
    Ext.util.Format.comboRenderer = function(combo, table, field, url) {
        return function(value,metadata,record,rowindex,colindex,store) {
            // choose what to render from the combobox store's data
            if (rowindex === 0) {
                var rec = combo.findRecord(combo.valueField, value);
                var retval = rec ? rec.get(combo.displayField) : value;
                return (retval.indexOf(fk_combo_comment) !== -1) ? value : retval;
            }

            if (value) {
                // create an id, attached to a span, and hang a button off it
                var id = Ext.id();
                // if the related table is "hidden" we are not passed field,
                // so this get() fails and the plain value is displayed
                var pk = record.get('cpac__pk_for_' + field);
                if (pk) {
                    createGridButton.defer(1, this, [id, table, url, pk]);
                    return ('<span id="' + id + '"></span>' + value);
                }
                return value;
            }
            return '';
        }
    };

    // used to hack an extra entry into the store results on filters
    var sfyRecord = Ext.data.Record.create([
        { name: 'dbid' }, { name: 'stringified' }
    ]);

    // create the combo instances
    [% FOREACH col IN cpac.tc.cols %]
      [% NEXT UNLESS cpac.tm.f.$col.is_foreign_key OR cpac.tm.f.$col.extra('is_reverse') %]
      [% NEXT IF cpac.tm.f.$col.extra('masked_by') %]
      var fk_combo_[% col | replace('\'', '\\\'') %] = new Ext.form.ComboBox ({
          valueField: 'dbid'
          ,displayField: 'stringified'
          ,hiddenName: 'combobox.[% col | replace('\'', '\\\'') %]'
          ,hiddenId: 'fk_combo_[% col | replace('\'', '\\\'') %]'
          ,loadingText: 'Searching...'
          ,forceSelection: true
          ,selectOnFocus: true
          ,typeAhead: false
          ,pageSize: 10
          ,triggerAction: 'all'
          ,lazyRender: true
          ,listClass: 'x-combo-list-small'
          ,lastQuery: ''
          ,store: new Ext.data.JsonStore ({
              [% IF cpac.tm.f.$col.extra('rel_type') == 'many_to_many' %]
                url: '[% c.uri_for(
                            c.controller('AutoCRUD::DisplayEngine::ExtJS2').action_for('list_stringified'),
                            [ cpac.g.site, cpac_db, cpac.tm.f.$col.extra('via').0 ]
                     ) %]'
              [% ELSE %]
                url: '[% c.uri_for(
                            c.controller('AutoCRUD::DisplayEngine::ExtJS2').action_for('list_stringified'),
                            [ cpac.g.site, cpac_db, cpac_table ]
                     ) %]'
              [% END %]
              ,root: 'rows'
              ,totalProperty: 'total'
              ,fields: [ 'dbid', 'stringified' ]
              ,listeners: {
                  beforeload: function(store, options) {
                      var start = options.params.start;
                      var limit = options.params.limit;
                      options.params.page = Math.floor(start / limit) + 1;
                      [% IF cpac.tm.f.$col.extra('rel_type') == 'many_to_many' %]
                        options.params.fkname = '[% cpac.tm.f.$col.extra('via').1 | replace('\'', '\\\'') %]';
                      [% ELSE %]
                        options.params.fkname = '[% col | replace('\'', '\\\'') %]';
                      [% END %]
                      if (fk_combo_[% col | replace('\'', '\\\'') %].getRawValue() && (options.params.page === 1)) {
                          // so that we can hack in an extra row
                          options.params.limit = 9;
                      }
                      return true;
                  }
                  ,load: function(store, records, options) {
                      var queryText = fk_combo_[% col | replace('\'', '\\\'') %].getRawValue();
                      if (queryText.length && (options.params.page === 1)) {
                          // be sure not to accidentally add a 2nd copy of fk_combo_comment
                          if (queryText.indexOf(fk_combo_comment) !== -1) {
                              queryText = queryText.replace(fk_combo_comment,'');
                          }
                          // insert an extra row with full-text-search option
                          store.insert(0, new sfyRecord({
                              dbid: queryText
                              ,stringified: queryText + fk_combo_comment
                          }));
                      }
                  }
              }
          })
          ,listeners: {
              // delete the previous query in the beforequery event or set
              // combo.lastQuery = null (this will reload the store the next time it expands)
              beforequery: function(qe) {
                  delete qe.combo.lastQuery;
              }
              ,blur : function() {
                  if (this.allowBlank && this.getRawValue() === '') {
                      this.clearValue();
                  }
              }
          }
      });
    [% END %]

    // we first process the main, ordinary cols (pk and others)
    // the do the one_to_many cols, and then add a column with a delete button

    var cm = new Ext.grid.ColumnModel([
      [% FOREACH col IN cpac.tc.cols %]
        [% ',' IF NOT loop.first %]{
          header: '[% cpac.tc.headings.$col %]'
          [% SET link = cpac.tm.f.$col.extra('ref_table') %]
          [% ',hidden: true' IF cpac.tc.hidden_cols.exists(col) %]
          [% IF (NOT cpac.tm.f.$col.extra('masked_by'))
                AND (cpac.tm.f.$col.is_foreign_key OR cpac.tm.f.$col.extra('is_reverse')) %]
            [% IF cpac.tm.f.$col.extra('rel_type').match('_many') %]
            ,dataIndex: '[% col | replace('\'', '\\\'') %]'
            ,editor: fk_combo_[% col | replace('\'', '\\\'') %] //reference to combo instance
            ,sortable: false
            ,menuDisabled: true
            ,tooltip: 'Hover mouse over a cell<br />to show related data'
            ,renderer: function (value,metadata,record,rowindex,colindex,store) {
                if (rowindex === 0) {
                    var combo = fk_combo_[% col | replace('\'', '\\\'') %]
                    var rec = combo.findRecord(combo.valueField, value);
                    var retval = rec ? rec.get(combo.displayField) : value;
                    return (retval.indexOf(fk_combo_comment) !== -1) ? value : retval;
                }
                else if (record.get('[% col | replace('\'', '\\\'') %]').join('') === '') { return '' }
                else {
                    metadata.css += 'half-grey';
                    metadata.attr += 'ext:qtitle="[% cpac.tc.headings.$col %]" ext:qtip="'
                        + record.get('[% col | replace('\'', '\\\'') %]').join('<br />') + '"';
                    return '<img ext:qtitle="[% cpac.tc.headings.$col %]" ext:qtip="'
                        + record.get('[% col | replace('\'', '\\\'') %]').join('<br />')
                        + '" src="[% c.uri_for( c.controller('AutoCRUD::Static').action_for('cpacstatic'),
                                        "magnifier.png" ) %]" />'
                        + ' Show';
                }
            }
            [% ELSE %]
            ,dataIndex: '[% col | replace('\'', '\\\'') %]'
            ,editor: fk_combo_[% col | replace('\'', '\\\'') %] //reference to combo instance
            // pass combo instance to reusable renderer
            [% SET ref_table = cpac.tm.f.$col.extra('ref_table') %]
            [% IF cpac.c.$cpac_db.t.$ref_table.hidden == 'yes' %]
              ,renderer: Ext.util.Format.comboRenderer(fk_combo_[% col %])
            [% ELSE %]
              // takes the combo-name, table display name (for tooltip), col
              // name (for retrieving FK's PK data), and target URL for POST
              ,renderer: Ext.util.Format.comboRenderer(
                  fk_combo_[% col %]
                  ,'[% cpac.c.$cpac_db.t.$ref_table.display_name %]'
                  ,'[% col | replace('\'', '\\\'') %]'
                  [% IF cpac.g.site == 'default' %]
                    ,'[% c.uri_for( c.controller('AutoCRUD::Root').action_for('table'), [cpac_db], ref_table ) %]'
                  [% ELSE %]
                    ,'[% c.uri_for( c.controller('AutoCRUD::Root').action_for('source'), [cpac.g.site,cpac_db], ref_table ) %]'
                  [% END %]
              )
            [% END %]
            [% END %]
          [% ELSE %]
            ,dataIndex: '[% col | replace('\'', '\\\'') %]'
            ,editor: new Ext.form.TextField({})
            ,renderer: function (value,metadata,record,rowindex,colindex,store) {
              [% IF cpac.tm.f.$col.extra('extjs_xtype') == 'checkbox' %]
                if (rowindex === 0) { return '' }
                if (value == '1') {
                  return '<img src="[% c.uri_for( c.controller('AutoCRUD::Static').action_for('cpacstatic'),
                                        "bullet_green.png" ) %]" />';
                }
                else {
                  return '<img src="[% c.uri_for( c.controller('AutoCRUD::Static').action_for('cpacstatic'),
                                        "bullet_red.png" ) %]" />';
                }
              [% ELSE %]
                return value;
              [% END %]
            }
          [% END %]
        }
      [% END %]
      [% IF cpac.tc.delete_allowed == 'yes' %]
        ,{
            header: 'Delete'
            ,width: 35
            ,dataIndex: 'cpac-delete-column'
            ,align: 'left'
            ,sortable: false
            ,menuDisabled: true
            ,renderer: function (value,metadata,record,rowindex,colindex,store) {
                if (rowindex === 0) { return '' }
                else {
                    metadata.attr += 'ext:qtitle="Delete" ext:qtip="' + record.get('cpac__display_name') + '"';
                    return '<img ext:qtitle="Delete" ext:qtip="' + record.get('cpac__display_name') + '"'
                        + ' src="[% c.uri_for( c.controller('AutoCRUD::Static').action_for('cpacstatic'),
                                        "bin_closed.png" ) %]" />';
                }
            }
        }
      [% END %]
    ]);

    // by default columns are sortable
    cm.defaultSortable = true;