How can I add multiple buttons in item list row?

I managed to add a button in the item list row. It’s pretty straightforward and works like a charm. However, I couldn’t find an option to add multiple buttons on the same row.

Is there any way I can add multiple buttons per row? Because I would like to add a print button as well.

In case anyone is wondering about the code,

frappe.listview_settings['Sales Invoice'] = frappe.listview_settings['Sales Invoice'] || {};

frappe.listview_settings['Sales Invoice'].button = {
    show(doc) {
        return doc.status != 'Paid';
    },
    get_label() {
        return __('Pay');
    },
    get_description(doc) {
        return __('Add Payment')
    },
    action(doc) {
        frappe.call({
            method: 'membership.api.make_payment_for_invoice',
            args: {
                invoice_name: doc.name,
            },
            callback: (r) => {
                
                if(r.exc) frappe.throw('Error creating payment');
                
                frappe.set_route('Form', 'Payment Entry', r.message.payment);
                
            }
        });
    }
}
2 Likes

As I understand it, the list view does not support multiple buttons out of the box. If you need that functionality, you’ll have to do a more involved customization.

How involved? Can I do that from my custom app? Or do I have to edit the frappe app itself?

I haven’t looked at it closely, but I suspect that all you’d need to do is extend the ListView class with some custom js to interpret a second button definition. Here’s the method you’d need to override:

The js could definitely be done in a custom app (and probably even client side with a client script), but you might have to make some css adjustments to give the second button space.

That’s it!! It works. Here is the code for anyone looking for the same feature. :heart_eyes:

my_app/public/js/list_view.js

frappe.views.ListView = class extends frappe.views.ListView {
    
    get_meta_html(doc) {
        let html = "";

        let settings_button = null;

        // check if the button property is an array or object
        if(Array.isArray(this.settings.button)) {
            // we have more than one button

            settings_button = '';
            for(const button of this.settings.button) {

                // make sure you have a unique name for each button,
                // otherwise it won't work
                // TODO make sure each name is unique, now it only checks if name exists
                if(!button.name) {
                    frappe.throw("Button needs a unique 'name' when using multiple buttons.");
                }

                if(button && button.show(doc)) {

                    settings_button += `
                        <span class="list-actions">
                            <button class="btn btn-action btn-default btn-xs"
                                data-name="${doc.name}" data-idx="${doc._idx}" data-action="${button.name}"
                                title="${button.get_description(doc)}">
                                ${button.get_label(doc)}
                            </button>
                        </span>
                    `;
                }

            }

        } else {
            // business as usual
            if (this.settings.button && this.settings.button.show(doc)) {
                settings_button = `
                    <span class="list-actions">
                        <button class="btn btn-action btn-default btn-xs"
                            data-name="${doc.name}" data-idx="${doc._idx}"
                            title="${this.settings.button.get_description(doc)}">
                            ${this.settings.button.get_label(doc)}
                        </button>
                    </span>
                `;

            }
        }


        const modified = comment_when(doc.modified, true);

        let assigned_to = `<div class="list-assignments">
            <span class="avatar avatar-small">
            <span class="avatar-empty"></span>
        </div>`;

        let assigned_users = JSON.parse(doc._assign || "[]");
        if (assigned_users.length) {
            assigned_to = `<div class="list-assignments">
                    ${frappe.avatar_group(assigned_users, 3, { filterable: true })[0].outerHTML}
                </div>`;
        }

        const comment_count = `<span class="${
            !doc._comment_count ? "text-extra-muted" : ""
        } comment-count">
                ${frappe.utils.icon('small-message')}
                ${doc._comment_count > 99 ? "99+" : doc._comment_count}
            </span>`;

        html += `
            <div class="level-item list-row-activity hidden-xs">
                <div class="hidden-md hidden-xs">
                    ${settings_button || assigned_to}
                </div>
                ${modified}
                ${comment_count}
            </div>
            <div class="level-item visible-xs text-right">
                ${this.get_indicator_dot(doc)}
            </div>
        `;

        return html;
    }

    setup_action_handler() {
        this.$result.on("click", ".btn-action", (e) => {
            const $button = $(e.currentTarget);
            const doc = this.data[$button.attr("data-idx")];
            
            // get the name of button
            const btnName = $button.attr('data-action');

            // again, check if array
            if(Array.isArray(this.settings.button)) {

                // find the button action
                const button = this.settings.button.find(b => b.name == btnName);
                button.action(doc);

            } else {
                this.settings.button.action(doc);
            }
            e.stopPropagation();
            return false;
        });
    }
}

my_app/public/js/sales_invoice_list.js

frappe.listview_settings['Sales Invoice'] = frappe.listview_settings['Sales Invoice'] || {};

frappe.listview_settings['Sales Invoice'].button = [
    {
        // provide unique name for the button to make setup_action_handler work
        name: 'btn-make-payment',

        show(doc) {
            return doc.status != 'Paid';
        },
        get_label() {
            return __('Pay');
        },
        get_description(doc) {
            return __('Add Payment')
        },
        action(doc) {
            frappe.call({
                method: 'membership.api.make_payment_for_invoice',
                args: {
                    invoice_name: doc.name,
                },
                callback: (r) => {
                    
                    if(r.exc) frappe.throw('Error creating payment');
                    
                    frappe.set_route('Form', 'Payment Entry', r.message.payment);
                    
                }
            });
        }
    },
    {
        name: 'btn-print-invoice',
        show(doc) { return true },
        get_label() { return __('Print'); },
        get_description(doc) { return __('Print Invoice') },
        action(doc) {
            frappe.set_route(`print/Sales Invoice/${doc.name}`);
        }
    },

]

This works fine, however I have to load this files in each of my doctype if I want to use multiple doctypes. So my hooks.py would look something like this.

doctype_list_js = {
	"Sales Invoice": [
		"public/js/frappe/list/list_view.js",
		"public/js/doctype_extend/sales_invoice/sales_invoice_list.js"
	],
	"Payment Entry": "public/js/doctype_extend/payment_entry/payment_entry_list.js"
}

I was unable to load list_view.js globally. But if I find a solution, I will post it here.

Thank you so much @peterg. This was really helpful.

6 Likes