Applying a custom filter when opening a drawer.

When viewing a Contact record, there is a subpanel called "Sites", listing Sites that the Contact is related to. I've been attempting to add an initial filter to the results of the drawer that appears when selecting the "Link Existing Record" option, to show only those Sites which are linked to the Account to which the Contact is also linked.

Following the filtering guides given here, I've managed to add a filter in .custom/Extension/modules/Sites/Ext/clients/base/filters/basic/FilterByAccountTemplate.php, which looks like this:

<?php

$viewdefs['Sites']['base']['filter']['basic']['filters'][] = array(
     'id' => 'FilterByAccountTemplate',
     'name' => 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
     'filter_definition' => array(
          array(
               <Linking Field> => array(
                    '$equals' => ''
               )
          )
     ),
     'editable' => true,
     'is_template' => true
);

And a display label in .custom/Extension/modules/Sites/Ext/Language/en_us.FilterByAccountTemplate.php, which looks like this:

<?php

$mod_strings['LBL_FILTER_BY_ACCOUNT_TEMPLATE'] = 'Account Sites';

So far, this is functional. When I post filter options to the right URL (e.g. https://<My Site>/rest/v10/Sites?<snip>&filter%5B0%5D%5B<Linking Field>%5D%5B%24equals%5D=<Account ID>), I'm getting back the right results.

But when I try to apply the filter in the GUI, no request is made; I'm simply getting all records returned, rather than filtered records. The filter displays correctly, and appears when the drawer is opened, but does not apply. My code for the front-end is in .custom/Modules/Sites/clients/base/fields/link-action/link-action.js, and looks like this:

({
    extendsFrom: 'LinkActionField',

    initialize: function(options) {
        this._super('initialize', [options]);
    },

    openSelectDrawer: function() {
        if (this.isDisabled()) {
            return;
        }
        var parentModel = this.context.get('parentModel');
     var linkModule = this.context.get('module');
        var link = this.context.get('link');
        var filterOptions = new app.utils.FilterOptions()
            .config({
                'initial_filter': 'FilterByAccountTemplate',
                'initial_filter_label': 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
                'filter_populate': {
                    '<Linking Field>': parentModel.get('account_id'),
                }
            })
            .format();
        var context = {
            module: linkModule,
            recParentModel: parentModel,
            recLink: link,
            recContext: this.context,
            recView: this.view,
            filterOptions: filterOptions
        }
        app.drawer.open({
            layout: 'multi-selection-list-link',
            context: context,
        });
    }
})

What am I doing wrong here? I've tried fiddling about with it and all I seem to get back is nothing. Applying and removing the filter doesn't make the client send any requests to the server.

Parents
  • I would try this, I might have over coded here but this should work if your filter is correct and all your '<Linking Field>' stuff is right and if your module is actually called 'Sites'.  Let me know.  The one you were building would have ended up (if it had worked) with a single select list (with radio buttons).  This one will produce a multi select list with check boxes.  It also MAY be only compatible with 7.7.  Not sure on that.

    /**
    * "Link existing record" action used in Subpanels.
    *
    * It needs to be sticky so that we keep things lined up nicely.
    *
    * @class View.Fields.Base.LinkActionField
    * @alias SUGAR.App.view.fields.BaseLinkActionField
    * @extends View.Fields.Base.StickyRowactionField
    */

    ({
        extendsFrom: 'StickyRowactionField',
        events: {
            'click a[name=select_button]': 'openSelectDrawer'
        },

        /**
         * Click handler for the select action.
         *
         * Opens a drawer for selecting records to link to the current record.
         */

        openSelectDrawer: function() {
            if (this.isDisabled()) {
                return;
            }

            app.drawer.open(
                this.getDrawerOptions(),
                _.bind(this.selectDrawerCallback, this)
            );
        },

        /**
         * Format drawer options used by {@link #openSelectDrawer}.
         *
         * By default it uses {@link View.Layouts.Base.SelectionListLayout} layout.
         * You can extend this method if you need to pass more or different options.
         *
         * @return {Object}
         * @return {string} return.module The module to select records from.
         * @return {Object} return.parent The parent context of the selection list
         *                                context to pass to the drawer.
         * @return {Data.Bean} return.recParentModel The current record to link to.
         * @return {string} return.recLink The relationship link.
         * @return {View.View} return.recView The view for the selection list.
         * @return {Backbone.Model} return.filterOptions The filter options object.
         * */

        getDrawerOptions: function() {
            var parentModel = this.context.get('parentModel');
            var linkModule = this.context.get('module');
            var link = this.context.get('link');

            var filterOptions = new app.utils.FilterOptions().config(this.def);
            filterOptions.setInitialFilter(this.def.initial_filter || '$relate');
            filterOptions.populateRelate(parentModel);

            //this code will execute for all Site subpanels, We want to apply the filter only for
            // Contacts

            if(_.isEqual(this.context.get('parentModel').get('_module'),"Contacts")){
                var filterOptions = new app.utils.FilterOptions()
                    .config({
                        'initial_filter': 'FilterByAccountTemplate',
                        'initial_filter_label': 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
                        'filter_populate': {
                            '<Linking Field>': parentModel.get('account_id'),
                        }
                    });
            }

            return {
                layout: 'multi-selection-list-link',
                context: {
                    module: linkModule,
                    recParentModel: parentModel,
                    recLink: link,
                    recContext: this.context,
                    recView: this.view,
                    independentMassCollection: true,
                    filterOptions: filterOptions.format()
                }
            };
        },

        /**
         * Callback method used when the drawer is closed.
         *
         * If a record has been selected, it makes a request to the server to link
         * it to the parent record.
         * On success, it refreshes the subpanel collection so the new record
         * appears in the subpanel.
         *
         * Finally, it expands the subpanel context by setting the `collapsed`
         * property to `false`.
         *
         * @param {Data.Bean} model The selected record to link to parent record.
         */

        selectDrawerCallback: function(model) {
            if (!model) {
                return;
            }

            var parentModel = this.context.get('parentModel');
            var link = this.context.get('link');

            var relatedModel = app.data.createRelatedBean(parentModel, model.id, link),
                options = {
                    //Show alerts for this request
                    showAlerts: true,
                    relate: true,
                    success: _.bind(function(model) {
                        //We've just linked a related, however, the list of records from
                        //loadData will come back in DESC (reverse chronological order
                        //with our newly linked on top). Hence, we reset pagination here.
                        this.context.get('collection').resetPagination();
                        this.context.set('collapsed', false);
                    }, this),
                    error: function(error) {
                        app.alert.show('server-error', {
                            level: 'error',
                            messages: 'ERR_GENERIC_SERVER_ERROR'
                        });
                    }
                };
            relatedModel.save(null, options);
        },

        /**
         * Check if link action should be disabled or not.
         *
         * The side effect of linking another record on a required relationship is
         * that the record could be already linked to a record and in that case we
         * would delete this existing link.
         *
         * @return {boolean} `true` if it should be disabled, `false` otherwise.
         * @override
         */

        isDisabled: function() {
            if (this._super('isDisabled')) {
                return true;
            }
            var link = this.context.get('link'),
                parentModule = this.context.get('parentModule'),
                required = app.utils.isRequiredLink(parentModule, link);
            return required;
        }
    })
  • I've managed to solve the problem and so am posting this in case another comes across the same issue in future.

    The field on which I was trying to filter wasn't explicitly declared in the default filter's $viewdefs array (set in ./custom/modules/Sites/clients/base/filters/default/default.php), and so, while the relation was valid, Sugar's JavaScript failed to find a field to apply the initial filter on, and submitted no server request. By adding the field to the array, the issue was solved.

    There are a couple other things I discovered along the way:

    • The $equals operator is likely to cease being used for relate fields (according to a comment within ./clients/base/views/filter-rows/filter-rows.js), so it's better to use $in instead.
    • I initially put my link-action.js extension in ./custom/Modules/... directories and my filter definitions in ./custom/Extension/modules/... directories, which works but was a mistake. I've now united them with each other so that future maintenance is more simple.
    • Based on what I can see from the Sugar core link-action.js source code, there should be a way of injecting an initial filter into the def variable for the drawer (i.e. the variable referenced by this.def in the getDrawerOptions function), which would be the best way of doing this, but I haven't figured out how to do that properly.
    • It was better to put my extended code in the getDrawerOptions function rather than the openSelectDrawer function, as that is where my changes really were.
    • I didn't need to override the initialize function or anything beyond getDrawerOptions, as missing functions are automatically delegated to LinkActionField.

    My final code looks like this:

    FilterByAccountTemplate.php

    <?php

    $viewdefs['Sites']['base']['filter']['basic']['filters'][] = array(
         'id' => 'FilterByAccountTemplate',
         'name' => 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
         'filter_definition' => array(
              array(
                   '<Linking Field>' => array(
                        '$in' => ''
                   )
              )
         ),
         'editable' => true,
         'is_template' => true
    );

    link-action.js

    ({
        extendsFrom: 'LinkActionField',

        getDrawerOptions: function() {
            var parentModel = this.context.get('parentModel');
            var linkModule = this.context.get('module');
            var link = this.context.get('link');
            var filterOptions;

            if(parentModel.module === 'Contacts') {
                filterOptions = new app.utils.FilterOptions()
                .config({
                    'initial_filter': 'FilterByAccountTemplate',
                    'initial_filter_label': 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
                    'filter_populate': {
                        '<Linking Field>': [parentModel.get('account_id')],
                    }
                });
            }
            else {
                filterOptions = new app.utils.FilterOptions().config(this.def);
                filterOptions.setInitialFilter(this.def.initial_filter || '$relate');
                filterOptions.populateRelate(parentModel);
            }

            return {
                layout: 'multi-selection-list-link',
                context: {
                    module: linkModule,
                    recParentModel: parentModel,
                    recLink: link,
                    recContext: this.context,
                    recView: this.view,
                    independentMassCollection: true,
                    filterOptions: filterOptions.format()
                }
            };
        }

    })
Reply
  • I've managed to solve the problem and so am posting this in case another comes across the same issue in future.

    The field on which I was trying to filter wasn't explicitly declared in the default filter's $viewdefs array (set in ./custom/modules/Sites/clients/base/filters/default/default.php), and so, while the relation was valid, Sugar's JavaScript failed to find a field to apply the initial filter on, and submitted no server request. By adding the field to the array, the issue was solved.

    There are a couple other things I discovered along the way:

    • The $equals operator is likely to cease being used for relate fields (according to a comment within ./clients/base/views/filter-rows/filter-rows.js), so it's better to use $in instead.
    • I initially put my link-action.js extension in ./custom/Modules/... directories and my filter definitions in ./custom/Extension/modules/... directories, which works but was a mistake. I've now united them with each other so that future maintenance is more simple.
    • Based on what I can see from the Sugar core link-action.js source code, there should be a way of injecting an initial filter into the def variable for the drawer (i.e. the variable referenced by this.def in the getDrawerOptions function), which would be the best way of doing this, but I haven't figured out how to do that properly.
    • It was better to put my extended code in the getDrawerOptions function rather than the openSelectDrawer function, as that is where my changes really were.
    • I didn't need to override the initialize function or anything beyond getDrawerOptions, as missing functions are automatically delegated to LinkActionField.

    My final code looks like this:

    FilterByAccountTemplate.php

    <?php

    $viewdefs['Sites']['base']['filter']['basic']['filters'][] = array(
         'id' => 'FilterByAccountTemplate',
         'name' => 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
         'filter_definition' => array(
              array(
                   '<Linking Field>' => array(
                        '$in' => ''
                   )
              )
         ),
         'editable' => true,
         'is_template' => true
    );

    link-action.js

    ({
        extendsFrom: 'LinkActionField',

        getDrawerOptions: function() {
            var parentModel = this.context.get('parentModel');
            var linkModule = this.context.get('module');
            var link = this.context.get('link');
            var filterOptions;

            if(parentModel.module === 'Contacts') {
                filterOptions = new app.utils.FilterOptions()
                .config({
                    'initial_filter': 'FilterByAccountTemplate',
                    'initial_filter_label': 'LBL_FILTER_BY_ACCOUNT_TEMPLATE',
                    'filter_populate': {
                        '<Linking Field>': [parentModel.get('account_id')],
                    }
                });
            }
            else {
                filterOptions = new app.utils.FilterOptions().config(this.def);
                filterOptions.setInitialFilter(this.def.initial_filter || '$relate');
                filterOptions.populateRelate(parentModel);
            }

            return {
                layout: 'multi-selection-list-link',
                context: {
                    module: linkModule,
                    recParentModel: parentModel,
                    recLink: link,
                    recContext: this.context,
                    recView: this.view,
                    independentMassCollection: true,
                    filterOptions: filterOptions.format()
                }
            };
        }

    })
Children
No Data