That’s it!! It works. Here is the code for anyone looking for the same feature.
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.