Add Listview Button for Configurable (Variable) Doctypes

Hello team,

Adding a button to the listview for a specific doctype is straight forward.

In my case I want to dynamically add a button to the listview of doctypes determined by some configuration e.g. I could define the doctypes I want to have the button for in hooks.py or even a custom settings doctype.

For this reason, it is not possible to determine the doctype name in advance and therefore not possible to do this:

frappe.listview_settings['Doctype Name'] = {
    onload: function(listview) {
         listview.page.add_inner_button(__('My Action'), function() {
             frappe.confirm(
                 'Are you sure you want to perform action?',
                 function() {
                     performAction();
                 }
             );
         });
    }
};

Problem

I want to have a single js file which i can dynamically hook to and create listview buttons for.

So far I have done this:

In hooks.py:

# My Doctypes 
# ------------------
my_doctypes = {
    "frappe": ["Contact", "Address"],
    "erpnext": ["Employee", "Customer", "Supplier", "Item", "Lead"],
    "healthcare": ["Patient"],
}
# Dynamically add relevant js to the doctypes
doctype_list_js = {}

for app, doctypes in my_doctypes.items():
    for doctype in doctypes:
        # List view scripting
        doctype_list_js[doctype] = "public/js/my_listview.js"

In my_doctypes.py:

@frappe.whitelist()
def get_my_doctypes():
    installed_apps = frappe.get_installed_apps()
    doctypes = frappe.get_hooks("my_doctypes")

    applicable_doctypes = set()
    for app in installed_apps:
        if app in doctypes:
            applicable_doctypes.update(doctypes[app])

    return list(applicable_doctypes)

In my_listview.js:

frappe.call({
    method: "path.to.my.get_my_doctypes",
    callback: function (r) {
        const doctypes = r.message || [];
        
        doctypes.forEach((doctype) => {
            console.log(`Adding listview settings for ${doctype}`);
            
            frappe.listview_settings[doctype] = {
                onload: function (listview) {
                    console.log(`loaded ${doctype} listview`);
                    
                    listview.page.add_inner_button(__("Do Something"), async () => {
                            await doSomething(doctype);
                        }
                    );
                    console.log(`Button added for ${doctype}`);
                },
            };
        });
    },
});

async function doSomething(doctype) {
    console.log(`Doing something for ${doctype}`);
    frappe.call({
        method: "path.to.my.method",
        args: { doctype },
        callback: function (r) {
            if (r.message && r.message.length > 0) {
                frappe.msgprint(__("Done."));
            } else {
                frappe.msgprint(__("Not done."));
            }
        },
    });
}

It only works randomly for some doctypes and not working most of the time. Maybe I am missing something. What would be the correct and reliable method to achieve this?

Any help is highly appreciated, thanks.

Hi there,

I think you might be running into a race condition. The frappe.call method in my_listview.js is asynchronous. Since it’s only getting called on list load by the doctype_list_js hook, your list may have already finished rendering before frappe.listview_settings gets updated.

Is there a reason you want to use the doctype_list_js hook here? I’d think app_include_js would be more appropriate (since these are site-level definitions).

I see. I have no particular reason to use doctype_list_js. Thanks, let me try that.