Skip to content

Instantly share code, notes, and snippets.

@esafwan
Last active June 2, 2024 21:46
Show Gist options
  • Save esafwan/c30e0c48af9e057e12d10e945ee0cd76 to your computer and use it in GitHub Desktop.
Save esafwan/c30e0c48af9e057e12d10e945ee0cd76 to your computer and use it in GitHub Desktop.
Frappe v15 List + Kanban Js Code

Frappe Kanban Implementation: basepath: frappe/frappe/public/js/frappe/views/kanban/

./kanban_view.js

import KanbanSettings from "./kanban_settings";

frappe.provide("frappe.views");

frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
	static load_last_view() {
		const route = frappe.get_route();
		if (route.length === 3) {
			const doctype = route[1];
			const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {};
			if (!user_settings.last_kanban_board) {
				return new frappe.views.KanbanView({ doctype: doctype });
			}

			route.push(user_settings.last_kanban_board);
			frappe.set_route(route);
			return true;
		}
		return false;
	}

	get view_name() {
		return "Kanban";
	}

	show() {
		frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) => {
			if (!kanbans.length) {
				return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
			} else if (kanbans.length && frappe.get_route().length !== 4) {
				return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
			} else {
				this.kanbans = kanbans;

				return frappe.run_serially([
					() => this.show_skeleton(),
					() => this.fetch_meta(),
					() => this.hide_skeleton(),
					() => this.check_permissions(),
					() => this.init(),
					() => this.before_refresh(),
					() => this.refresh(),
				]);
			}
		});
	}

	init() {
		return super.init().then(() => {
			let menu_length = this.page.menu.find(".dropdown-item").length;
			if (menu_length === 1) {
				// Only 'Refresh' (hidden) is present (always), dropdown is visibly empty
				this.page.hide_menu();
			}
		});
	}

	setup_defaults() {
		return super.setup_defaults().then(() => {
			let get_board_name = () => {
				return this.kanbans.length && this.kanbans[0].name;
			};

			this.board_name = frappe.get_route()[3] || get_board_name() || null;
			this.page_title = __(this.board_name);
			this.card_meta = this.get_card_meta();
			this.page_length = 0;

			return frappe.run_serially([
				() => this.set_board_perms_and_push_menu_items(),
				() => this.get_board(),
			]);
		});
	}

	set_board_perms_and_push_menu_items() {
		// needs server-side call as client-side document instance is absent before kanban render
		return frappe.call({
			method: "frappe.client.get_doc_permissions",
			args: {
				doctype: "Kanban Board",
				docname: this.board_name,
			},
			callback: (result) => {
				this.board_perms = result.message.permissions || {};
				this.push_menu_items();
			},
		});
	}

	push_menu_items() {
		if (this.board_perms.write) {
			this.menu_items.push({
				label: __("Save filters"),
				action: () => {
					this.save_kanban_board_filters();
				},
			});
		}

		if (this.board_perms.delete) {
			this.menu_items.push({
				label: __("Delete Kanban Board"),
				action: () => {
					frappe.confirm(__("Are you sure you want to proceed?"), () => {
						frappe.db.delete_doc("Kanban Board", this.board_name).then(() => {
							frappe.show_alert(`Kanban Board ${this.board_name} deleted.`);
							frappe.set_route("List", this.doctype, "List");
						});
					});
				},
			});
		}
	}

	setup_paging_area() {
		// pass
	}

	toggle_result_area() {
		this.$result.toggle(this.data.length > 0);
	}

	get_board() {
		return frappe.db.get_doc("Kanban Board", this.board_name).then((board) => {
			this.board = board;
			this.board.filters_array = JSON.parse(this.board.filters || "[]");
			this.board.fields = JSON.parse(this.board.fields || "[]");
			this.filters = this.board.filters_array;
		});
	}

	setup_page() {
		this.hide_sidebar = true;
		this.hide_page_form = true;
		this.hide_card_layout = true;
		this.hide_sort_selector = true;
		super.setup_page();
	}

	setup_view() {
		if (this.board.columns.length > 5) {
			this.page.container.addClass("full-width");
		}
		this.setup_realtime_updates();
		this.setup_like();
	}

	set_fields() {
		super.set_fields();
		this._add_field(this.card_meta.title_field);
	}

	before_render() {
		frappe.model.user_settings.save(this.doctype, "last_view", this.view_name);
		this.save_view_user_settings({
			last_kanban_board: this.board_name,
		});
	}

	render_list() {}

	on_filter_change() {
		if (!this.board_perms.write) return; // avoid misleading ux

		if (JSON.stringify(this.board.filters_array) !== JSON.stringify(this.filter_area.get())) {
			this.page.set_indicator(__("Not Saved"), "orange");
		} else {
			this.page.clear_indicator();
		}
	}

	save_kanban_board_filters() {
		const filters = this.filter_area.get();

		frappe.db.set_value("Kanban Board", this.board_name, "filters", filters).then((r) => {
			if (r.exc) {
				frappe.show_alert({
					indicator: "red",
					message: __("There was an error saving filters"),
				});
				return;
			}
			frappe.show_alert({
				indicator: "green",
				message: __("Filters saved"),
			});

			this.board.filters_array = filters;
			this.on_filter_change();
		});
	}

	get_fields() {
		this.fields.push([this.board.field_name, this.board.reference_doctype]);
		return super.get_fields();
	}

	render() {
		const board_name = this.board_name;
		if (!this.kanban) {
			this.kanban = new frappe.views.KanbanBoard({
				doctype: this.doctype,
				board: this.board,
				board_name: board_name,
				cards: this.data,
				card_meta: this.card_meta,
				wrapper: this.$result,
				cur_list: this,
				user_settings: this.view_user_settings,
			});
		} else if (board_name === this.kanban.board_name) {
			this.kanban.update(this.data);
		}
	}

	get_card_meta() {
		var meta = frappe.get_meta(this.doctype);
		// preserve route options erased by new doc
		let route_options = { ...frappe.route_options };
		var doc = frappe.model.get_new_doc(this.doctype);
		frappe.route_options = route_options;
		var title_field = null;
		var quick_entry = false;

		if (this.meta.title_field) {
			title_field = frappe.meta.get_field(this.doctype, this.meta.title_field);
		}

		this.meta.fields.forEach((df) => {
			const is_valid_field =
				["Data", "Text", "Small Text", "Text Editor"].includes(df.fieldtype) && !df.hidden;

			if (is_valid_field && !title_field) {
				// can be mapped to textarea
				title_field = df;
			}
		});

		// quick entry
		var mandatory = meta.fields.filter((df) => df.reqd && !doc[df.fieldname]);

		if (
			mandatory.some((df) => frappe.model.table_fields.includes(df.fieldtype)) ||
			mandatory.length > 1
		) {
			quick_entry = true;
		}

		if (!title_field) {
			title_field = frappe.meta.get_field(this.doctype, "name");
		}

		return {
			quick_entry: quick_entry,
			title_field: title_field,
		};
	}

	get_view_settings() {
		return {
			label: __("Kanban Settings", null, "Button in kanban view menu"),
			action: () => this.show_kanban_settings(),
			standard: true,
		};
	}

	show_kanban_settings() {
		frappe.model.with_doctype(this.doctype, () => {
			new KanbanSettings({
				kanbanview: this,
				doctype: this.doctype,
				settings: this.board,
				meta: frappe.get_meta(this.doctype),
			});
		});
	}

	get required_libs() {
		return "kanban_board.bundle.js";
	}
};

frappe.views.KanbanView.get_kanbans = function (doctype) {
	let kanbans = [];

	return get_kanban_boards().then((kanban_boards) => {
		if (kanban_boards) {
			kanban_boards.forEach((board) => {
				let route = `/app/${frappe.router.slug(board.reference_doctype)}/view/kanban/${
					board.name
				}`;
				kanbans.push({ name: board.name, route: route });
			});
		}

		return kanbans;
	});

	function get_kanban_boards() {
		return frappe
			.call("frappe.desk.doctype.kanban_board.kanban_board.get_kanban_boards", { doctype })
			.then((r) => r.message);
	}
};

frappe.views.KanbanView.show_kanban_dialog = function (doctype) {
	let dialog = new_kanban_dialog();
	dialog.show();

	function make_kanban_board(board_name, field_name, project) {
		return frappe.call({
			method: "frappe.desk.doctype.kanban_board.kanban_board.quick_kanban_board",
			args: {
				doctype,
				board_name,
				field_name,
				project,
			},
			callback: function (r) {
				var kb = r.message;
				if (kb.filters) {
					frappe.provide("frappe.kanban_filters");
					frappe.kanban_filters[kb.kanban_board_name] = kb.filters;
				}
				frappe.set_route("List", doctype, "Kanban", kb.kanban_board_name);
			},
		});
	}

	function new_kanban_dialog() {
		/* Kanban dialog can show either "Save" or "Customize Form" option depending if any Select fields exist in the DocType for Kanban creation
		 */

		const select_fields = frappe.get_meta(doctype).fields.filter((df) => {
			return df.fieldtype === "Select" && df.fieldname !== "kanban_column";
		});
		const dialog_fields = get_fields_for_dialog(select_fields);
		const to_save = select_fields.length > 0;
		const primary_action_label = to_save ? __("Save") : __("Customize Form");
		const dialog_title = to_save ? __("New Kanban Board") : __("No Select Field Found");

		let primary_action = () => {
			if (to_save) {
				const values = dialog.get_values();
				make_kanban_board(values.board_name, values.field_name, values.project).then(
					() => dialog.hide(),
					(err) => frappe.msgprint(err)
				);
			} else {
				frappe.set_route("Form", "Customize Form", { doc_type: doctype });
			}
		};

		return new frappe.ui.Dialog({
			title: dialog_title,
			fields: dialog_fields,
			primary_action_label,
			primary_action,
		});
	}

	function get_fields_for_dialog(select_fields) {
		if (!select_fields.length) {
			return [
				{
					fieldtype: "HTML",
					options: `
					<div>
						<p class="text-medium">
						${__(
							'No fields found that can be used as a Kanban Column. Use the Customize Form to add a Custom Field of type "Select".'
						)}
						</p>
					</div>
				`,
				},
			];
		}

		let fields = [
			{
				fieldtype: "Data",
				fieldname: "board_name",
				label: __("Kanban Board Name"),
				reqd: 1,
				description: ["Note", "ToDo"].includes(doctype)
					? __("This Kanban Board will be private")
					: "",
			},
			{
				fieldtype: "Select",
				fieldname: "field_name",
				label: __("Columns based on"),
				options: select_fields.map((df) => ({ label: df.label, value: df.fieldname })),
				default: select_fields[0],
				reqd: 1,
			},
		];

		if (doctype === "Task") {
			fields.push({
				fieldtype: "Link",
				fieldname: "project",
				label: __("Project"),
				options: "Project",
			});
		}

		return fields;
	}
};

./kanban_column.html

<div class="kanban-column" data-column-value="{{title}}">
	<div class="kanban-column-header">
		<span class="kanban-column-title">
			<span class="indicator-pill {{indicator}}"></span>
			<span class="kanban-title ellipsis" title="{{title}}">{{ __(title) }}</span>
		</span>
		<div class="column-options dropdown pull-right">
			<a data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
				<svg class="icon icon-sm">
					<use href="#icon-dot-horizontal"></use>
				</svg>
			</a>
			<ul class="dropdown-menu" style="max-height: 300px; overflow-y: auto;">
				<li><a class="dropdown-item" data-action="archive">{{ __("Archive") }}</a></li>
			</ul>
		</div>
	</div>
	<div class="add-card">
		<div class="ellipsis">
			+ {{ __("Add {0}", [__(doctype)]) }}
		</div>
	</div>
	<div class="kanban-card new-card-area">
		<textarea name="title"></textarea>
	</div>
	<div class="kanban-cards">
	</div>
</div>

./kanban_card.html

<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
	<div class="kanban-card content">
		{% if(image_url) { %}
		<div class="kanban-image">
			<img  src="{{image_url}}" alt="{{title}}">
		</div>
		{% } %}
		<div class="kanban-card-body">
			<div class="kanban-title-area">
				<a href="{{ form_link }}">
					<div class="kanban-card-title ellipsis" title="{{title}}">
						{{ title }}
					</div>
				</a>
				<br>
				<div class="kanban-card-doc text-muted">
					{{ doc_content }}
				</div>
			</div>
			<div class="kanban-card-meta">
			</div>
		</div>
	</div>
</div>

./kanban_board.html

<div class="kanban">
	<div class="kanban-column add-new-column">
		<div class="kanban-column-title compose-column">
			<a> + {{ __("Add Column") }}</a>
		</div>
		<form class="compose-column-form kanban-column-title">
			<input class="new-column-title" name="title" type="text" autocomplete="off">
		</form>
	</div>
	<div class="kanban-empty-state text-muted text-center" style="display: none;">
		{{ __("Loading...") }}
	</div>
</div>

./kanban_board.bundle.js

// TODO: Refactor for better UX

import { createStore } from "vuex";

frappe.provide("frappe.views");

(function () {
	var method_prefix = "frappe.desk.doctype.kanban_board.kanban_board.";

	let columns_unwatcher = null;

	var store = createStore({
		state: {
			doctype: "",
			board: {},
			card_meta: {},
			cards: [],
			columns: [],
			filters_modified: false,
			cur_list: {},
			empty_state: true,
		},
		mutations: {
			update_state(state, obj) {
				Object.assign(state, obj);
			},
		},
		actions: {
			init: function (context, opts) {
				context.commit("update_state", {
					empty_state: true,
				});
				var board = opts.board;
				var card_meta = opts.card_meta;
				opts.card_meta = card_meta;
				opts.board = board;
				var cards = opts.cards.map(function (card) {
					return prepare_card(card, opts);
				});
				var columns = prepare_columns(board.columns);
				context.commit("update_state", {
					doctype: opts.doctype,
					board: board,
					card_meta: card_meta,
					cards: cards,
					columns: columns,
					cur_list: opts.cur_list,
					empty_state: false,
					wrapper: opts.wrapper,
				});
			},
			update_cards: function (context, cards) {
				var state = context.state;
				var _cards = cards
					.map((card) => prepare_card(card, state))
					.concat(state.cards)
					.uniqBy((card) => card.name);

				context.commit("update_state", {
					cards: _cards,
				});
			},
			add_column: function (context, col) {
				if (frappe.model.can_create("Custom Field")) {
					store.dispatch("update_column", { col, action: "add" });
				} else {
					frappe.msgprint({
						title: __("Not permitted"),
						message: __("You are not allowed to create columns"),
						indicator: "red",
					});
				}
			},
			archive_column: function (context, col) {
				store.dispatch("update_column", { col, action: "archive" });
			},
			restore_column: function (context, col) {
				store.dispatch("update_column", { col, action: "restore" });
			},
			update_column: function (context, { col, action }) {
				var doctype = context.state.doctype;
				var board = context.state.board;
				fetch_customization(doctype)
					.then(function (doc) {
						return modify_column_field_in_c11n(doc, board, col.title, action);
					})
					.then(save_customization)
					.then(function () {
						return update_kanban_board(board.name, col.title, action);
					})
					.then(
						function (r) {
							var cols = r.message;
							context.commit("update_state", {
								columns: prepare_columns(cols),
							});
						},
						function (err) {
							console.error(err);
						}
					);
			},
			add_card: function (context, { card_title, column_title }) {
				var state = context.state;
				var doc = frappe.model.get_new_doc(state.doctype);
				var field = state.card_meta.title_field;
				var quick_entry = state.card_meta.quick_entry;

				var doc_fields = {};
				doc_fields[field.fieldname] = card_title;
				doc_fields[state.board.field_name] = column_title;
				state.cur_list.filter_area.get().forEach(function (f) {
					if (f[2] !== "=") return;
					doc_fields[f[1]] = f[3];
				});

				$.extend(doc, doc_fields);

				// add the card directly
				// for better ux
				const card = prepare_card(doc, state);
				card._disable_click = true;
				const cards = [...state.cards, card];
				// remember the name which we will override later
				const old_name = doc.name;
				context.commit("update_state", { cards });

				if (field && !quick_entry) {
					return insert_doc(doc).then(function (r) {
						// update the card in place with the updated doc
						const updated_doc = r.message;
						const index = state.cards.findIndex((card) => card.name === old_name);
						const card = prepare_card(updated_doc, state);
						const new_cards = state.cards.slice();
						new_cards[index] = card;
						context.commit("update_state", { cards: new_cards });
						const args = {
							new: 1,
							name: card.name,
							colname: updated_doc[state.board.field_name],
						};
						store.dispatch("update_order_for_single_card", args);
					});
				} else {
					frappe.new_doc(state.doctype, doc);
				}
			},
			update_card: function (context, card) {
				var index = -1;
				context.state.cards.forEach(function (c, i) {
					if (c.name === card.name) {
						index = i;
					}
				});
				var cards = context.state.cards.slice();
				if (index !== -1) {
					cards.splice(index, 1, card);
				}
				context.commit("update_state", { cards: cards });
			},
			update_order_for_single_card: function (context, card) {
				// cache original order
				const _cards = context.state.cards.slice();
				const _columns = context.state.columns.slice();
				let args = {};
				let method_name = "";

				if (card.new) {
					method_name = "add_card";
					args = {
						board_name: context.state.board.name,
						docname: card.name,
						colname: card.colname,
					};
				} else {
					method_name = "update_order_for_single_card";
					args = {
						board_name: context.state.board.name,
						docname: card.name,
						from_colname: card.from_colname,
						to_colname: card.to_colname,
						old_index: card.old_index,
						new_index: card.new_index,
					};
				}
				frappe.dom.freeze();
				frappe
					.call({
						method: method_prefix + method_name,
						args: args,
						callback: (r) => {
							let board = r.message;
							let updated_cards = [
								{ name: card.name, column: card.to_colname || card.colname },
							];
							let cards = update_cards_column(updated_cards);
							let columns = prepare_columns(board.columns);
							context.commit("update_state", {
								cards: cards,
								columns: columns,
							});
							frappe.dom.unfreeze();
						},
					})
					.fail(function () {
						// revert original order
						context.commit("update_state", {
							cards: _cards,
							columns: _columns,
						});
						frappe.dom.unfreeze();
					});
			},
			update_order: function (context) {
				// cache original order
				const _cards = context.state.cards.slice();
				const _columns = context.state.columns.slice();

				const order = {};
				context.state.wrapper.find(".kanban-column[data-column-value]").each(function () {
					var col_name = $(this).data().columnValue;
					order[col_name] = [];
					$(this)
						.find(".kanban-card-wrapper")
						.each(function () {
							var card_name = decodeURIComponent($(this).data().name);
							order[col_name].push(card_name);
						});
				});

				frappe
					.call({
						method: method_prefix + "update_order",
						args: {
							board_name: context.state.board.name,
							order: order,
						},
						callback: (r) => {
							var board = r.message[0];
							var updated_cards = r.message[1];
							var cards = update_cards_column(updated_cards);
							var columns = prepare_columns(board.columns);
							context.commit("update_state", {
								cards: cards,
								columns: columns,
							});
						},
					})
					.fail(function () {
						// revert original order
						context.commit("update_state", {
							cards: _cards,
							columns: _columns,
						});
					});
			},
			update_column_order: function (context, order) {
				return frappe
					.call({
						method: method_prefix + "update_column_order",
						args: {
							board_name: context.state.board.name,
							order: order,
						},
					})
					.then(function (r) {
						var board = r.message;
						var columns = prepare_columns(board.columns);
						context.commit("update_state", {
							columns: columns,
						});
					});
			},
			set_indicator: function (context, { column, color }) {
				return frappe
					.call({
						method: method_prefix + "set_indicator",
						args: {
							board_name: context.state.board.name,
							column_name: column.title,
							indicator: color,
						},
					})
					.then(function (r) {
						var board = r.message;
						var columns = prepare_columns(board.columns);
						context.commit("update_state", {
							columns: columns,
						});
					});
			},
		},
	});

	frappe.views.KanbanBoard = function (opts) {
		var self = {};
		self.wrapper = opts.wrapper;
		self.cur_list = opts.cur_list;
		self.board_name = opts.board_name;
		self.board_perms = self.cur_list.board_perms;

		self.update = function (cards) {
			// update cards internally
			opts.cards = cards;

			if (self.wrapper.find(".kanban").length > 0 && self.cur_list.start !== 0) {
				store.dispatch("update_cards", cards);
			} else {
				init();
			}
		};

		function init() {
			store.dispatch("init", opts);
			columns_unwatcher && columns_unwatcher();
			store.watch((state, getters) => {
				return state.columns;
			}, make_columns);
			prepare();
			make_columns();
			store.watch((state, getters) => {
				return state.cur_list;
			}, setup_restore_columns);
			columns_unwatcher = store.watch((state, getters) => {
				return state.columns;
			}, setup_restore_columns);
			store.watch((state, getters) => {
				return state.empty_state;
			}, show_empty_state);

			store.dispatch("update_order");
		}

		function prepare() {
			self.$kanban_board = self.wrapper.find(".kanban");

			if (self.$kanban_board.length === 0) {
				self.$kanban_board = $(frappe.render_template("kanban_board"));
				self.$kanban_board.appendTo(self.wrapper);
			}

			self.$filter_area = self.cur_list.$page.find(".active-tag-filters");
			bind_events();
			setup_sortable();
		}

		function make_columns() {
			self.$kanban_board.find(".kanban-column").not(".add-new-column").remove();
			var columns = store.state.columns;

			columns.filter(is_active_column).map(function (col) {
				frappe.views.KanbanBoardColumn(col, self.$kanban_board, self.board_perms);
			});
		}

		function bind_events() {
			bind_add_column();
			bind_clickdrag();
		}

		function setup_sortable() {
			// If no write access to board, editing board (by dragging column) should be blocked
			if (!self.board_perms.write) return;

			var sortable = new Sortable(self.$kanban_board.get(0), {
				group: "columns",
				animation: 150,
				dataIdAttr: "data-column-value",
				filter: ".add-new-column",
				handle: ".kanban-column-title",
				onEnd: function () {
					var order = sortable.toArray();
					order = order.slice(1);
					store.dispatch("update_column_order", order);
				},
			});
		}

		function bind_add_column() {
			if (!self.board_perms.write) {
				// If no write access to board, editing board (by adding column) should be blocked
				self.$kanban_board.find(".add-new-column").remove();
				return;
			}

			var $add_new_column = self.$kanban_board.find(".add-new-column"),
				$compose_column = $add_new_column.find(".compose-column"),
				$compose_column_form = $add_new_column.find(".compose-column-form").hide();

			$compose_column.on("click", function () {
				$(this).hide();
				$compose_column_form.show();
				$compose_column_form.find("input").focus();
			});

			//save on enter
			$compose_column_form.keydown(function (e) {
				if (e.which == 13) {
					e.preventDefault();
					if (!frappe.request.ajax_count) {
						// not already working -- double entry
						var title = $compose_column_form.serializeArray()[0].value;
						var col = {
							title: title.trim(),
						};
						store.dispatch("add_column", col);
						$compose_column_form.find("input").val("");
						$compose_column.show();
						$compose_column_form.hide();
					}
				}
			});

			// on form blur
			$compose_column_form.find("input").on("blur", function () {
				$(this).val("");
				$compose_column.show();
				$compose_column_form.hide();
			});
		}

		function bind_clickdrag() {
			let isDown = false;
			let startX;
			let scrollLeft;
			let draggable = self.$kanban_board[0];

			draggable.addEventListener("mousedown", (e) => {
				// don't trigger scroll if one of the ancestors of the
				// clicked element matches any of these selectors
				let ignoreEl = [
					".kanban-column .kanban-column-header",
					".kanban-column .add-card",
					".kanban-column .kanban-card.new-card-area",
					".kanban-card-wrapper",
				];
				if (ignoreEl.some((el) => e.target.closest(el))) return;

				isDown = true;
				draggable.classList.add("clickdrag-active");
				startX = e.pageX - draggable.offsetLeft;
				scrollLeft = draggable.scrollLeft;
			});
			draggable.addEventListener("mouseleave", () => {
				isDown = false;
				draggable.classList.remove("clickdrag-active");
			});
			draggable.addEventListener("mouseup", () => {
				isDown = false;
				draggable.classList.remove("clickdrag-active");
			});
			draggable.addEventListener("mousemove", (e) => {
				if (!isDown) return;
				e.preventDefault();
				const x = e.pageX - draggable.offsetLeft;
				const walk = x - startX;
				draggable.scrollLeft = scrollLeft - walk;
			});
		}

		function setup_restore_columns() {
			var cur_list = store.state.cur_list;
			var columns = store.state.columns;
			var list_row_right = cur_list.$page
				.find(`[data-list-renderer='Kanban'] .list-row-right`)
				.css("margin-right", "15px");
			list_row_right.empty();

			var archived_columns = columns.filter(function (col) {
				return col.status === "Archived";
			});

			if (!archived_columns.length) return;

			var options = archived_columns.reduce(function (a, b) {
				return (
					a +
					`<li><a class='option'>" +
					"<span class='ellipsis' style='max-width: 100px; display: inline-block'>" +
					__(b.title) + "</span>" +
					"<button style='float:right;' data-column='" + b.title +
					"' class='btn btn-default btn-xs restore-column text-muted'>"
					+ __('Restore') + "</button></a></li>`
				);
			}, "");
			var $dropdown = $(
				"<div class='dropdown pull-right'>" +
					"<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" +
					"<span class='dropdown-text'>" +
					__("Archived Columns") +
					"</span><i class='caret'></i></a>" +
					"<ul class='dropdown-menu'>" +
					options +
					"</ul>" +
					"</div>"
			);

			list_row_right.html($dropdown);

			$dropdown.find(".dropdown-menu").on("click", "button.restore-column", function () {
				var column_title = $(this).data().column;
				var col = {
					title: column_title,
					status: "Archived",
				};
				store.dispatch("restore_column", col);
			});
		}

		function show_empty_state() {
			var empty_state = store.state.empty_state;

			if (empty_state) {
				self.$kanban_board.find(".kanban-column").hide();
				self.$kanban_board.find(".kanban-empty-state").show();
			} else {
				self.$kanban_board.find(".kanban-column").show();
				self.$kanban_board.find(".kanban-empty-state").hide();
			}
		}

		init();

		return self;
	};

	frappe.views.KanbanBoardColumn = function (column, wrapper, board_perms) {
		var self = {};
		var filtered_cards = [];

		function init() {
			make_dom();
			setup_sortable();
			make_cards();
			store.watch((state, getters) => {
				return state.cards;
			}, make_cards);
			bind_add_card();
			bind_options();
		}

		function make_dom() {
			self.$kanban_column = $(
				frappe.render_template("kanban_column", {
					title: column.title,
					doctype: store.state.doctype,
					indicator: frappe.scrub(column.indicator, "-"),
				})
			).appendTo(wrapper);
			// add task, archive
			self.$kanban_cards = self.$kanban_column.find(".kanban-cards");
		}

		function make_cards() {
			self.$kanban_cards.empty();
			var cards = store.state.cards;
			filtered_cards = get_cards_for_column(cards, column);
			var filtered_cards_names = filtered_cards.map((card) => card.name);

			var order = column.order;
			if (order) {
				order = JSON.parse(order);
				// new cards
				filtered_cards.forEach(function (card) {
					if (order.indexOf(card.name) === -1) {
						frappe.views.KanbanBoardCard(card, self.$kanban_cards);
					}
				});
				order.forEach(function (name) {
					if (!filtered_cards_names.includes(name)) return;
					frappe.views.KanbanBoardCard(get_card(name), self.$kanban_cards);
				});
			} else {
				filtered_cards.map(function (card) {
					frappe.views.KanbanBoardCard(card, self.$kanban_cards);
				});
			}
		}

		function setup_sortable() {
			// Block card dragging/record editing without 'write' access to reference doctype
			if (!frappe.model.can_write(store.state.doctype)) return;

			Sortable.create(self.$kanban_cards.get(0), {
				group: "cards",
				animation: 150,
				dataIdAttr: "data-name",
				forceFallback: true,
				onStart: function () {
					wrapper.find(".kanban-card.add-card").fadeOut(200, function () {
						wrapper.find(".kanban-cards").height("100vh");
					});
				},
				onEnd: function (e) {
					wrapper.find(".kanban-card.add-card").fadeIn(100);
					wrapper.find(".kanban-cards").height("auto");
					// update order
					const args = {
						name: decodeURIComponent($(e.item).attr("data-name")),
						from_colname: $(e.from)
							.parents(".kanban-column")
							.attr("data-column-value"),
						to_colname: $(e.to).parents(".kanban-column").attr("data-column-value"),
						old_index: e.oldIndex,
						new_index: e.newIndex,
					};
					store.dispatch("update_order_for_single_card", args);
				},
				onAdd: function () {},
			});
		}

		function bind_add_card() {
			var $wrapper = self.$kanban_column;
			var $btn_add = $wrapper.find(".add-card");
			var $new_card_area = $wrapper.find(".new-card-area");

			if (!frappe.model.can_create(store.state.doctype)) {
				// Block record/card creation without 'create' access to reference doctype
				$btn_add.remove();
				$new_card_area.remove();
				return;
			}

			var $textarea = $new_card_area.find("textarea");

			//Add card button
			$new_card_area.hide();
			$btn_add.on("click", function () {
				$btn_add.hide();
				$new_card_area.show();
				$textarea.focus();
			});

			//save on enter
			$new_card_area.keydown(function (e) {
				if (e.which == 13) {
					e.preventDefault();
					if (!frappe.request.ajax_count) {
						// not already working -- double entry
						e.preventDefault();
						var card_title = $textarea.val();
						$new_card_area.hide();
						$textarea.val("");
						store
							.dispatch("add_card", {
								card_title,
								column_title: column.title,
							})
							.then(() => {
								$btn_add.show();
							});
					}
				}
			});

			// on textarea blur
			$textarea.on("blur", function () {
				$(this).val("");
				$btn_add.show();
				$new_card_area.hide();
			});
		}

		function bind_options() {
			if (!board_perms.write) {
				// If no write access to board, column options should be hidden
				self.$kanban_column.find(".column-options").remove();
				return;
			}

			self.$kanban_column
				.find(".column-options .dropdown-menu")
				.on("click", "[data-action]", function () {
					var $btn = $(this);
					var action = $btn.data().action;

					if (action === "archive") {
						store.dispatch("archive_column", column);
					} else if (action === "indicator") {
						var color = $btn.data().indicator;
						store.dispatch("set_indicator", { column, color });
					}
				});

			get_column_indicators(function (indicators) {
				let html = `<li class="button-group">${indicators
					.map((indicator) => {
						let classname = frappe.scrub(indicator, "-");
						return `<div data-action="indicator" data-indicator="${indicator}" class="btn btn-default btn-xs indicator-pill ${classname}"></div>`;
					})
					.join("")}</li>`;
				self.$kanban_column.find(".column-options .dropdown-menu").append(html);
			});
		}

		init();
	};

	frappe.views.KanbanBoardCard = function (card, wrapper) {
		var self = {};

		function init() {
			if (!card) return;
			make_dom();
			render_card_meta();
		}

		function make_dom() {
			var opts = {
				name: card.name,
				title: frappe.utils.html2text(card.title),
				disable_click: card._disable_click ? "disable-click" : "",
				creation: card.creation,
				doc_content: get_doc_content(card),
				image_url: cur_list.get_image_url(card),
				form_link: frappe.utils.get_form_link(card.doctype, card.name),
			};

			self.$card = $(frappe.render_template("kanban_card", opts)).appendTo(wrapper);

			if (!frappe.model.can_write(card.doctype)) {
				// Undraggable card without 'write' access to reference doctype
				self.$card.find(".kanban-card-body").css("cursor", "default");
			}
		}

		function get_doc_content(card) {
			let fields = [];
			for (let field_name of cur_list.board.fields) {
				let field =
					frappe.meta.docfield_map[card.doctype]?.[field_name] ||
					frappe.model.get_std_field(field_name);
				let label = cur_list.board.show_labels
					? `<span>${__(field.label, null, field.parent)}: </span>`
					: "";
				let value = frappe.format(card.doc[field_name], field);
				fields.push(`
					<div class="text-muted text-truncate">
						${label}
						<span>${value}</span>
					</div>
				`);
			}

			return fields.join("");
		}

		function get_tags_html(card) {
			return card.tags
				? `<div class="kanban-tags">
					${cur_list.get_tags_html(card.tags, 3, true)}
				</div>`
				: "";
		}

		function render_card_meta() {
			let html = get_tags_html(card);

			if (card.comment_count > 0)
				html += `<span class="list-comment-count small text-muted ">
					${frappe.utils.icon("es-line-chat-alt")}
					${card.comment_count}
				</span>`;

			const $assignees_group = get_assignees_group();

			html += `
				<span class="kanban-assignments"></span>
				${cur_list.get_like_html(card)}
			`;

			if (card.color && frappe.ui.color.validate_hex(card.color)) {
				const $div = $("<div>");
				$("<div></div>")
					.css({
						width: "30px",
						height: "4px",
						borderRadius: "2px",
						marginBottom: "8px",
						backgroundColor: card.color,
					})
					.appendTo($div);

				self.$card.find(".kanban-card .kanban-title-area").prepend($div);
			}

			self.$card
				.find(".kanban-card-meta")
				.empty()
				.append(html)
				.find(".kanban-assignments")
				.append($assignees_group);
		}

		function get_assignees_group() {
			return frappe.avatar_group(card.assigned_list, 3, {
				css_class: "avatar avatar-small",
				action_icon: "add",
				action: show_assign_to_dialog,
			});
		}

		function show_assign_to_dialog(e) {
			e.preventDefault();
			e.stopPropagation();
			self.assign_to = new frappe.ui.form.AssignToDialog({
				obj: self,
				method: "frappe.desk.form.assign_to.add",
				doctype: card.doctype,
				docname: card.name,
				callback: function () {
					const users = self.assign_to_dialog.get_values().assign_to;
					card.assigned_list = [...new Set(card.assigned_list.concat(users))];
					store.dispatch("update_card", card);
				},
			});
			self.assign_to_dialog = self.assign_to.dialog;
			self.assign_to_dialog.show();
		}

		init();
	};

	function prepare_card(card, state, doc) {
		var assigned_list = card._assign ? JSON.parse(card._assign) : [];
		var comment_count = card._comment_count || 0;

		if (doc) {
			card = Object.assign({}, card, doc);
		}

		return {
			doctype: state.doctype,
			name: card.name,
			title: card[state.card_meta.title_field.fieldname],
			creation: moment(card.creation).format("MMM DD, YYYY"),
			_liked_by: card._liked_by,
			image: card[cur_list.meta.image_field],
			tags: card._user_tags,
			column: card[state.board.field_name],
			assigned_list: card.assigned_list || assigned_list,
			comment_count: card.comment_count || comment_count,
			color: card.color || null,
			doc: doc || card,
		};
	}

	function prepare_columns(columns) {
		return columns.map(function (col) {
			return {
				title: col.column_name,
				status: col.status,
				order: col.order,
				indicator: col.indicator || "gray",
			};
		});
	}

	function modify_column_field_in_c11n(doc, board, title, action) {
		doc.fields.forEach(function (df) {
			if (df.fieldname === board.field_name && df.fieldtype === "Select") {
				if (!df.options) df.options = "";

				if (action === "add") {
					//add column_name to Select field's option field
					if (!df.options.includes(title)) df.options += "\n" + title;
				} else if (action === "delete") {
					var options = df.options.split("\n");
					var index = options.indexOf(title);
					if (index !== -1) options.splice(index, 1);
					df.options = options.join("\n");
				}
			}
		});
		return doc;
	}

	function fetch_customization(doctype) {
		return new Promise(function (resolve) {
			frappe.model.with_doc("Customize Form", "Customize Form", function () {
				var doc = frappe.get_doc("Customize Form");
				doc.doc_type = doctype;
				frappe.call({
					doc: doc,
					method: "fetch_to_customize",
					callback: function (r) {
						resolve(r.docs[0]);
					},
				});
			});
		});
	}

	function save_customization(doc) {
		if (!doc) return;
		doc.hide_success = true;
		return frappe.call({
			doc: doc,
			method: "save_customization",
		});
	}

	function insert_doc(doc) {
		return frappe.call({
			method: "frappe.client.insert",
			args: {
				doc: doc,
			},
			callback: function () {
				frappe.model.clear_doc(doc.doctype, doc.name);
				frappe.show_alert({ message: __("Saved"), indicator: "green" }, 1);
			},
		});
	}

	function update_kanban_board(board_name, column_title, action) {
		var method;
		var args = {
			board_name: board_name,
			column_title: column_title,
		};
		if (action === "add") {
			method = "add_column";
		} else if (action === "archive" || action === "restore") {
			method = "archive_restore_column";
			args.status = action === "archive" ? "Archived" : "Active";
		}
		return frappe.call({
			method: method_prefix + method,
			args: args,
		});
	}

	function is_active_column(col) {
		return col.status !== "Archived";
	}

	function get_cards_for_column(cards, column) {
		return cards.filter(function (card) {
			return card.column === column.title;
		});
	}

	function get_card(name) {
		return store.state.cards.find(function (c) {
			return c.name === name;
		});
	}

	function update_cards_column(updated_cards) {
		var cards = store.state.cards;
		cards.forEach(function (c) {
			updated_cards.forEach(function (uc) {
				if (uc.name === c.name) {
					c.column = uc.column;
				}
			});
		});
		return cards;
	}

	function get_column_indicators(callback) {
		frappe.model.with_doctype("Kanban Board Column", function () {
			var meta = frappe.get_meta("Kanban Board Column");
			var indicators;
			meta.fields.forEach(function (df) {
				if (df.fieldname === "indicator") {
					indicators = df.options.split("\n");
				}
			});
			if (!indicators) {
				//
				indicators = ["green", "blue", "orange", "gray"];
			}
			callback(indicators);
		});
	}
})();

Frappe List View Implmentation on top of which Kanban is implemented: basepath: frappe/frappe/public/js/frappe/list

./base_list.js

frappe.provide("frappe.views");

frappe.views.BaseList = class BaseList {
	constructor(opts) {
		Object.assign(this, opts);
	}

	show() {
		return frappe.run_serially([
			() => this.show_skeleton(),
			() => this.fetch_meta(),
			() => this.hide_skeleton(),
			() => this.check_permissions(),
			() => this.init(),
			() => this.before_refresh(),
			() => this.refresh(),
		]);
	}

	init() {
		if (this.init_promise) return this.init_promise;

		let tasks = [
			this.setup_defaults,
			this.set_stats,
			this.setup_fields,
			// make view
			this.setup_page,
			this.setup_side_bar,
			this.setup_main_section,
			this.setup_view,
			this.setup_view_menu,
		].map((fn) => fn.bind(this));

		this.init_promise = frappe.run_serially(tasks);
		return this.init_promise;
	}

	setup_defaults() {
		this.page_name = frappe.get_route_str();
		this.page_title = this.page_title || frappe.router.doctype_layout || __(this.doctype);
		this.meta = frappe.get_meta(this.doctype);
		this.settings = frappe.listview_settings[this.doctype] || {};
		this.user_settings = frappe.get_user_settings(this.doctype);

		this.start = 0;
		this.page_length = frappe.is_large_screen() ? 100 : 20;
		this.data = [];
		this.method = "frappe.desk.reportview.get";

		this.can_create = frappe.model.can_create(this.doctype);
		this.can_write = frappe.model.can_write(this.doctype);

		this.fields = [];
		this.filters = [];
		this.sort_by = this.meta.sort_field || "modified";
		this.sort_order = this.meta.sort_order || "desc";

		// Setup buttons
		this.primary_action = null;
		this.secondary_action = null;

		this.menu_items = [
			{
				label: __("Refresh"),
				action: () => this.refresh(),
				class: "visible-xs",
			},
		];
	}

	get_list_view_settings() {
		return frappe
			.call("frappe.desk.listview.get_list_settings", {
				doctype: this.doctype,
			})
			.then((doc) => (this.list_view_settings = doc.message || {}));
	}

	async setup_fields() {
		await this.set_fields();
		this.build_fields();
	}

	async set_fields() {
		let fields = [].concat(frappe.model.std_fields_list, this.meta.title_field);

		fields.forEach((f) => this._add_field(f));
	}

	get_fields_in_list_view() {
		return this.meta.fields.filter((df) => {
			return (
				(frappe.model.is_value_type(df.fieldtype) &&
					df.in_list_view &&
					frappe.perm.has_perm(this.doctype, df.permlevel, "read")) ||
				(df.fieldtype === "Currency" && df.options && !df.options.includes(":")) ||
				df.fieldname === "status"
			);
		});
	}

	build_fields() {
		// fill in missing doctype
		this.fields = this.fields.map((f) => {
			if (typeof f === "string") {
				f = [f, this.doctype];
			}
			return f;
		});
		// remove null or undefined values
		this.fields = this.fields.filter(Boolean);
		//de-duplicate
		this.fields = this.fields.uniqBy((f) => f[0] + f[1]);
	}

	_add_field(fieldname, doctype) {
		if (!fieldname) return;

		if (!doctype) doctype = this.doctype;

		if (typeof fieldname === "object") {
			// df is passed
			const df = fieldname;
			fieldname = df.fieldname;
			doctype = df.parent || doctype;
		}

		if (!this.fields) this.fields = [];
		const is_valid_field =
			frappe.model.std_fields_list.includes(fieldname) ||
			frappe.meta.has_field(doctype, fieldname) ||
			fieldname === "_seen";

		let is_virtual = this.meta.fields.find((df) => df.fieldname == fieldname)?.is_virtual;

		if (!is_valid_field || is_virtual) {
			return;
		}

		this.fields.push([fieldname, doctype]);
	}

	set_stats() {
		this.stats = ["_user_tags"];
		// add workflow field (as priority)
		this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype);
		if (this.workflow_state_fieldname) {
			if (!frappe.workflow.workflows[this.doctype]["override_status"]) {
				this._add_field(this.workflow_state_fieldname);
			}
			this.stats.push(this.workflow_state_fieldname);
		}
	}

	fetch_meta() {
		return frappe.model.with_doctype(this.doctype);
	}

	show_skeleton() {}

	hide_skeleton() {}

	check_permissions() {
		return true;
	}

	setup_page() {
		this.page = this.parent.page;
		this.$page = $(this.parent);
		!this.hide_card_layout && this.page.main.addClass("frappe-card");
		this.page.page_form.removeClass("row").addClass("flex");
		this.hide_page_form && this.page.page_form.hide();
		this.hide_sidebar && this.$page.addClass("no-list-sidebar");
		this.setup_page_head();
	}

	setup_page_head() {
		this.set_title();
		this.set_menu_items();
		this.set_breadcrumbs();
	}

	set_title() {
		this.page.set_title(this.page_title, null, true, "", this.meta?.description);
	}

	setup_view_menu() {
		// TODO: add all icons
		const icon_map = {
			Image: "image-view",
			List: "list",
			Report: "small-file",
			Calendar: "calendar",
			Gantt: "gantt",
			Kanban: "kanban",
			Dashboard: "dashboard",
			Map: "map",
		};

		if (frappe.boot.desk_settings.view_switcher && !this.meta.force_re_route_to_default_view) {
			/* @preserve
			for translation, don't remove
			__("List View") __("Report View") __("Dashboard View") __("Gantt View"),
			__("Kanban View") __("Calendar View") __("Image View") __("Inbox View"),
			__("Tree View") __("Map View") */
			this.views_menu = this.page.add_custom_button_group(
				__("{0} View", [this.view_name]),
				icon_map[this.view_name] || "list"
			);
			this.views_list = new frappe.views.ListViewSelect({
				doctype: this.doctype,
				parent: this.views_menu,
				page: this.page,
				list_view: this,
				sidebar: this.list_sidebar,
				icon_map: icon_map,
			});
		}
	}

	set_default_secondary_action() {
		if (this.secondary_action) {
			const $secondary_action = this.page.set_secondary_action(
				this.secondary_action.label,
				this.secondary_action.action,
				this.secondary_action.icon
			);
			if (!this.secondary_action.icon) {
				$secondary_action.addClass("hidden-xs");
			} else if (!this.secondary_action.label) {
				$secondary_action.addClass("visible-xs");
			}
		} else {
			this.refresh_button = this.page.add_action_icon(
				"es-line-reload",
				() => {
					this.refresh();
				},
				"",
				__("Reload List")
			);
		}
	}

	set_menu_items() {
		this.set_default_secondary_action();

		this.menu_items &&
			this.menu_items.map((item) => {
				if (item.condition && item.condition() === false) {
					return;
				}
				const $item = this.page.add_menu_item(
					item.label,
					item.action,
					item.standard,
					item.shortcut
				);
				if (item.class) {
					$item && $item.addClass(item.class);
				}
			});
	}

	set_breadcrumbs() {
		frappe.breadcrumbs.add(this.meta.module, this.doctype);
	}

	setup_side_bar() {
		if (this.hide_sidebar || !frappe.boot.desk_settings.list_sidebar) return;
		this.list_sidebar = new frappe.views.ListSidebar({
			doctype: this.doctype,
			stats: this.stats,
			parent: this.$page.find(".layout-side-section"),
			page: this.page,
			list_view: this,
		});
	}

	toggle_side_bar(show) {
		let show_sidebar = show || JSON.parse(localStorage.show_sidebar || "true");
		show_sidebar = !show_sidebar;
		localStorage.show_sidebar = show_sidebar;
		this.show_or_hide_sidebar();
		$(document.body).trigger("toggleListSidebar");
	}

	show_or_hide_sidebar() {
		let show_sidebar = JSON.parse(localStorage.show_sidebar || "true");
		$(document.body).toggleClass("no-list-sidebar", !show_sidebar);
	}

	setup_main_section() {
		return frappe.run_serially(
			[
				this.setup_list_wrapper,
				this.show_or_hide_sidebar,
				this.setup_filter_area,
				this.setup_sort_selector,
				this.setup_result_area,
				this.setup_no_result_area,
				this.setup_freeze_area,
				this.setup_paging_area,
			].map((fn) => fn.bind(this))
		);
	}

	setup_list_wrapper() {
		this.$frappe_list = $('<div class="frappe-list">').appendTo(this.page.main);
	}

	setup_filter_area() {
		if (this.hide_filters) return;
		this.filter_area = new FilterArea(this);

		if (this.filters && this.filters.length > 0) {
			return this.filter_area.set(this.filters).catch(() => {
				this.filter_area.clear(false);
			});
		}
	}

	setup_sort_selector() {
		if (this.hide_sort_selector) return;
		this.sort_selector = new frappe.ui.SortSelector({
			parent: this.$filter_section,
			doctype: this.doctype,
			args: {
				sort_by: this.sort_by,
				sort_order: this.sort_order,
			},
			onchange: this.on_sort_change.bind(this),
		});
	}

	on_sort_change() {
		this.refresh();
	}

	setup_result_area() {
		this.$result = $(`<div class="result">`);
		this.$frappe_list.append(this.$result);
	}

	setup_no_result_area() {
		this.$no_result = $(`
			<div class="no-result text-muted flex justify-center align-center">
				${this.get_no_result_message()}
			</div>
		`).hide();
		this.$frappe_list.append(this.$no_result);
	}

	setup_freeze_area() {
		this.$freeze = $('<div class="freeze"></div>').hide();
		this.$frappe_list.append(this.$freeze);
	}

	get_no_result_message() {
		return __("Nothing to show");
	}

	setup_paging_area() {
		const paging_values = [20, 100, 500, 2500];
		this.$paging_area = $(
			`<div class="list-paging-area level">
				<div class="level-left">
					<div class="btn-group">
						${paging_values
							.map(
								(value) => `
							<button type="button" class="btn btn-default btn-sm btn-paging"
								data-value="${value}">
								${value}
							</button>
						`
							)
							.join("")}
					</div>
				</div>
				<div class="level-right">
					<button class="btn btn-default btn-more btn-sm">
						${__("Load More")}
					</button>
				</div>
			</div>`
		).hide();
		this.$frappe_list.append(this.$paging_area);

		// set default paging btn active
		this.$paging_area
			.find(`.btn-paging[data-value="${this.page_length}"]`)
			.addClass("btn-info");

		this.$paging_area.on("click", ".btn-paging", (e) => {
			const $this = $(e.currentTarget);

			// set active button
			this.$paging_area.find(".btn-paging").removeClass("btn-info");
			$this.addClass("btn-info");

			this.start = 0;
			this.page_length = this.selected_page_count = $this.data().value;

			this.refresh();
		});

		this.$paging_area.on("click", ".btn-more", (e) => {
			this.start += this.page_length;
			this.page_length = this.selected_page_count || 20;
			this.refresh();
		});
	}

	get_fields() {
		// convert [fieldname, Doctype] => tabDoctype.fieldname
		return this.fields.map((f) => frappe.model.get_full_column_name(f[0], f[1]));
	}

	get_group_by() {
		let name_field = this.fields && this.fields.find((f) => f[0] == "name");
		if (name_field) {
			return frappe.model.get_full_column_name(name_field[0], name_field[1]);
		}
		return null;
	}

	setup_view() {
		// for child classes
	}

	get_filter_value(fieldname) {
		const filter = this.get_filters_for_args().filter((f) => f[1] == fieldname)[0];
		if (!filter) return;
		return (
			{
				like: filter[3]?.replace(/^%?|%$/g, ""),
				"not set": null,
			}[filter[2]] || filter[3]
		);
	}

	get_filters_for_args() {
		// filters might have a fifth param called hidden,
		// we don't want to pass that server side
		return this.filter_area ? this.filter_area.get().map((filter) => filter.slice(0, 4)) : [];
	}

	get_args() {
		let filters = this.get_filters_for_args();
		let group_by = this.get_group_by();
		let group_by_required =
			Array.isArray(filters) &&
			filters.some((filter) => {
				return filter[0] !== this.doctype;
			});
		return {
			doctype: this.doctype,
			fields: this.get_fields(),
			filters,
			order_by: this.sort_selector && this.sort_selector.get_sql_string(),
			start: this.start,
			page_length: this.page_length,
			view: this.view,
			group_by: group_by_required ? group_by : null,
		};
	}

	get_call_args() {
		const args = this.get_args();
		return {
			method: this.method,
			args: args,
			freeze: this.freeze_on_refresh || false,
			freeze_message: this.freeze_message || __("Loading") + "...",
		};
	}

	before_refresh() {
		// modify args here just before making the request
		// see list_view.js
	}

	refresh() {
		let args = this.get_call_args();
		if (this.no_change(args)) {
			// console.log('throttled');
			return Promise.resolve();
		}
		this.freeze(true);
		// fetch data from server
		return frappe.call(args).then((r) => {
			// render
			this.prepare_data(r);
			this.toggle_result_area();
			this.before_render();
			this.render();
			this.after_render();
			this.freeze(false);
			this.reset_defaults();
			if (this.settings.refresh) {
				this.settings.refresh(this);
			}
		});
	}

	no_change(args) {
		// returns true if arguments are same for the last 3 seconds
		// this helps in throttling if called from various sources
		if (this.last_args && JSON.stringify(args) === this.last_args) {
			return true;
		}
		this.last_args = JSON.stringify(args);
		setTimeout(() => {
			this.last_args = null;
		}, 3000);
		return false;
	}

	prepare_data(r) {
		let data = r.message || {};

		// extract user_info for assignments
		Object.assign(frappe.boot.user_info, data.user_info);
		delete data.user_info;

		data = !Array.isArray(data) ? frappe.utils.dict(data.keys, data.values) : data;

		if (this.start === 0) {
			this.data = data;
		} else {
			this.data = this.data.concat(data);
		}

		this.data = this.data.uniqBy((d) => d.name);
	}

	reset_defaults() {
		this.page_length = this.page_length + this.start;
		this.start = 0;
	}

	freeze() {
		// show a freeze message while data is loading
	}

	before_render() {}

	after_render() {}

	render() {
		// for child classes
	}

	on_filter_change() {
		// fired when filters are added or removed
	}

	toggle_result_area() {
		this.$result.toggle(this.data.length > 0);
		this.$paging_area.toggle(this.data.length > 0);
		this.$no_result.toggle(this.data.length == 0);

		const show_more = this.start + this.page_length <= this.data.length;
		this.$paging_area.find(".btn-more").toggle(show_more);
	}

	call_for_selected_items(method, args = {}) {
		args.names = this.get_checked_items(true);

		frappe.call({
			method: method,
			args: args,
			freeze: true,
			callback: (r) => {
				if (!r.exc) {
					this.refresh();
				}
			},
		});
	}
};

class FilterArea {
	constructor(list_view) {
		this.list_view = list_view;
		this.list_view.page.page_form.append(`<div class="standard-filter-section flex"></div>`);

		const filter_area = this.list_view.hide_page_form
			? this.list_view.page.custom_actions
			: this.list_view.page.page_form;

		this.list_view.$filter_section = $('<div class="filter-section flex">').appendTo(
			filter_area
		);

		this.$filter_list_wrapper = this.list_view.$filter_section;
		this.trigger_refresh = true;

		this.debounced_refresh_list_view = frappe.utils.debounce(
			this.refresh_list_view.bind(this),
			300
		);
		this.setup();
	}

	setup() {
		if (!this.list_view.hide_page_form) this.make_standard_filters();
		this.make_filter_list();
	}

	get() {
		let filters = this.filter_list.get_filters();
		let standard_filters = this.get_standard_filters();

		return filters.concat(standard_filters).uniqBy(JSON.stringify);
	}

	set(filters) {
		// use to method to set filters without triggering refresh
		this.trigger_refresh = false;
		return this.add(filters, false).then(() => {
			this.trigger_refresh = true;
			this.filter_list.update_filter_button();
		});
	}

	add(filters, refresh = true) {
		if (!filters || (Array.isArray(filters) && filters.length === 0)) return Promise.resolve();

		if (typeof filters[0] === "string") {
			// passed in the format of doctype, field, condition, value
			const filter = Array.from(arguments);
			filters = [filter];
		}

		filters = filters.filter((f) => !this.exists(f));

		// standard filters = filters visible on list view
		// non-standard filters = filters set by filter button
		const { non_standard_filters, promise } = this.set_standard_filter(filters);

		return promise
			.then(() => {
				return (
					non_standard_filters.length > 0 &&
					this.filter_list.add_filters(non_standard_filters)
				);
			})
			.then(() => {
				refresh && this.list_view.refresh();
			});
	}

	refresh_list_view() {
		if (this.trigger_refresh) {
			this.list_view.start = 0;
			this.list_view.refresh();
			this.list_view.on_filter_change();
		}
	}

	exists(f) {
		let exists = false;
		// check in standard filters
		const fields_dict = this.list_view.page.fields_dict;
		if (f[2] === "=" && f[1] in fields_dict) {
			const value = fields_dict[f[1]].get_value();
			if (value) {
				exists = true;
			}
		}

		// check in filter area
		if (!exists) {
			exists = this.filter_list.filter_exists(f);
		}

		return exists;
	}

	set_standard_filter(filters) {
		if (filters.length === 0) {
			return {
				non_standard_filters: [],
				promise: Promise.resolve(),
			};
		}

		const fields_dict = this.list_view.page.fields_dict;

		return filters.reduce((out, filter) => {
			const [dt, fieldname, condition, value] = filter;
			out.promise = out.promise || Promise.resolve();
			out.non_standard_filters = out.non_standard_filters || [];

			// set in list view area if filters are present
			// don't set like filter on link fields (gets reset)
			if (
				fields_dict[fieldname] &&
				(condition === "=" ||
					(condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link") ||
					(condition === "descendants of (inclusive)" &&
						fields_dict[fieldname]?.df?.fieldtype == "Link"))
			) {
				// standard filter
				out.promise = out.promise.then(() => fields_dict[fieldname].set_value(value));
			} else {
				// filter out non standard filters
				out.non_standard_filters.push(filter);
			}
			return out;
		}, {});
	}

	remove_filters(filters) {
		filters.map((f) => {
			this.remove(f[1]);
		});
	}

	remove(fieldname) {
		const fields_dict = this.list_view.page.fields_dict;

		if (fieldname in fields_dict) {
			fields_dict[fieldname].set_value("");
		}

		let filter = this.filter_list.get_filter(fieldname);
		if (filter) filter.remove();
		this.filter_list.apply();
		return Promise.resolve();
	}

	clear(refresh = true) {
		if (!refresh) {
			this.trigger_refresh = false;
		}

		this.filter_list.clear_filters();

		const promises = [];
		const fields_dict = this.list_view.page.fields_dict;
		for (let key in fields_dict) {
			const field = this.list_view.page.fields_dict[key];
			promises.push(() => field.set_value(""));
		}
		return frappe.run_serially(promises).then(() => {
			this.trigger_refresh = true;
		});
	}

	make_standard_filters() {
		this.standard_filters_wrapper = this.list_view.page.page_form.find(
			".standard-filter-section"
		);
		let fields = [];

		if (!this.list_view.settings.hide_name_filter) {
			fields.push({
				fieldtype: "Data",
				label: "ID",
				condition: "like",
				fieldname: "name",
				onchange: () => this.debounced_refresh_list_view(),
			});
		}

		if (this.list_view.custom_filter_configs) {
			this.list_view.custom_filter_configs.forEach((config) => {
				config.onchange = () => this.debounced_refresh_list_view();
			});

			fields = fields.concat(this.list_view.custom_filter_configs);
		}

		const doctype_fields = this.list_view.meta.fields;
		const title_field = this.list_view.meta.title_field;

		fields = fields.concat(
			doctype_fields
				.filter(
					(df) =>
						df.fieldname === title_field ||
						(df.in_standard_filter && frappe.model.is_value_type(df.fieldtype))
				)
				.map((df) => {
					let options = df.options;
					let condition = "=";
					let fieldtype = df.fieldtype;
					if (
						[
							"Text",
							"Small Text",
							"Text Editor",
							"HTML Editor",
							"Data",
							"Code",
							"Phone",
							"JSON",
							"Read Only",
						].includes(fieldtype)
					) {
						fieldtype = "Data";
						condition = "like";
					}
					if (df.fieldtype == "Select" && df.options) {
						options = df.options.split("\n");
						if (options.length > 0 && options[0] != "") {
							options.unshift("");
							options = options.join("\n");
						}
					}
					if (
						df.fieldtype == "Link" &&
						df.options &&
						frappe.boot.treeviews.includes(df.options)
					) {
						condition = "descendants of (inclusive)";
					}

					return {
						fieldtype: fieldtype,
						label: __(df.label, null, df.parent),
						options: options,
						fieldname: df.fieldname,
						condition: condition,
						onchange: () => this.debounced_refresh_list_view(),
						ignore_link_validation: fieldtype === "Dynamic Link",
						is_filter: 1,
					};
				})
		);

		fields.map((df) => {
			this.list_view.page.add_field(df, this.standard_filters_wrapper);
		});
	}

	get_standard_filters() {
		const filters = [];
		const fields_dict = this.list_view.page.fields_dict;
		for (let key in fields_dict) {
			let field = fields_dict[key];
			let value = field.get_value();
			if (value) {
				if (field.df.condition === "like" && !value.includes("%")) {
					value = "%" + value + "%";
				}
				filters.push([
					field.df.doctype || this.list_view.doctype,
					field.df.fieldname,
					field.df.condition || "=",
					value,
				]);
			}
		}

		return filters;
	}

	make_filter_list() {
		$(`<div class="filter-selector">
			<div class="btn-group">
				<button class="btn btn-default btn-sm filter-button">
					<span class="filter-icon">
						${frappe.utils.icon("es-line-filter")}
					</span>
					<span class="button-label hidden-xs">
					${__("Filter")}
					<span>
				</button>
				<button class="btn btn-default btn-sm filter-x-button" title="${__("Clear all filters")}">
					<span class="filter-icon">
						${frappe.utils.icon("es-small-close")}
					</span>
				</button>
			</div>
		</div>`).appendTo(this.$filter_list_wrapper);

		this.filter_button = this.$filter_list_wrapper.find(".filter-button");
		this.filter_x_button = this.$filter_list_wrapper.find(".filter-x-button");
		this.filter_list = new frappe.ui.FilterGroup({
			base_list: this.list_view,
			parent: this.$filter_list_wrapper,
			doctype: this.list_view.doctype,
			filter_button: this.filter_button,
			filter_x_button: this.filter_x_button,
			default_filters: [],
			on_change: () => this.debounced_refresh_list_view(),
		});
	}

	is_being_edited() {
		// returns true if user is currently editing filters
		return (
			this.filter_list &&
			this.filter_list.wrapper &&
			this.filter_list.wrapper.find(".filter-box:visible").length > 0
		);
	}
}

// utility function to validate view modes
frappe.views.view_modes = [
	"List",
	"Report",
	"Dashboard",
	"Gantt",
	"Kanban",
	"Calendar",
	"Image",
	"Inbox",
	"Tree",
	"Map",
];
frappe.views.is_valid = (view_mode) => frappe.views.view_modes.includes(view_mode);

./list_view.js

import BulkOperations from "./bulk_operations";
import ListSettings from "./list_settings";

frappe.provide("frappe.views");

frappe.views.ListView = class ListView extends frappe.views.BaseList {
	static load_last_view() {
		const route = frappe.get_route();
		const doctype = route[1];

		if (route.length === 2) {
			const user_settings = frappe.get_user_settings(doctype);
			const last_view = user_settings.last_view;
			frappe.set_route(
				"list",
				frappe.router.doctype_layout || doctype,
				frappe.views.is_valid(last_view) ? last_view.toLowerCase() : "list"
			);
			return true;
		}
		return false;
	}

	constructor(opts) {
		super(opts);
		this.show();
		this.debounced_refresh = frappe.utils.debounce(
			this.process_document_refreshes.bind(this),
			2000
		);
		this.count_upper_bound = 1001;
		this._element_factory = new ElementFactory(this.doctype);
	}

	has_permissions() {
		return frappe.perm.has_perm(this.doctype, 0, "read");
	}

	show() {
		this.parent.disable_scroll_to_top = true;
		super.show();
	}

	check_permissions() {
		if (!this.has_permissions()) {
			frappe.set_route("");
			frappe.throw(__("Not permitted to view {0}", [this.doctype]));
		}
	}

	show_skeleton() {
		this.$list_skeleton = this.parent.page.container.find(".list-skeleton");
		if (!this.$list_skeleton.length) {
			this.$list_skeleton = $(`
				<div class="row list-skeleton">
					<div class="col-lg-2">
						<div class="list-skeleton-box"></div>
					</div>
					<div class="col">
						<div class="list-skeleton-box"></div>
					</div>
				</div>
			`);
			this.parent.page.container.find(".page-content").append(this.$list_skeleton);
		}
		this.parent.page.container.find(".layout-main").hide();
		this.$list_skeleton.show();
	}

	hide_skeleton() {
		this.$list_skeleton && this.$list_skeleton.hide();
		this.parent.page.container.find(".layout-main").show();
	}

	get view_name() {
		return "List";
	}

	get view_user_settings() {
		return this.user_settings[this.view_name] || {};
	}

	setup_defaults() {
		super.setup_defaults();

		this.view = "List";
		// initialize with saved order by
		this.sort_by = this.view_user_settings.sort_by || this.sort_by || "modified";
		this.sort_order = this.view_user_settings.sort_order || this.sort_order || "desc";

		// build menu items
		this.menu_items = this.menu_items.concat(this.get_menu_items());

		// set filters from view_user_settings or list_settings
		if (Array.isArray(this.view_user_settings.filters)) {
			// Priority 1: view_user_settings
			const saved_filters = this.view_user_settings.filters;
			this.filters = this.validate_filters(saved_filters);
		} else {
			// Priority 2: filters in listview_settings
			this.filters = (this.settings.filters || []).map((f) => {
				if (f.length === 3) {
					f = [this.doctype, f[0], f[1], f[2]];
				}
				return f;
			});
		}

		if (this.view_name == "List") this.toggle_paging = true;

		this.patch_refresh_and_load_lib();
		return this.get_list_view_settings();
	}

	on_sort_change(sort_by, sort_order) {
		this.sort_by = sort_by;
		this.sort_order = sort_order;
		super.on_sort_change();
	}

	validate_filters(filters) {
		let valid_fields = this.meta.fields.map((df) => df.fieldname);
		valid_fields = valid_fields.concat(frappe.model.std_fields_list);
		return filters.filter((f) => valid_fields.includes(f[1])).uniqBy((f) => f[1]);
	}

	setup_page() {
		this.parent.list_view = this;
		super.setup_page();
	}

	setup_page_head() {
		super.setup_page_head();
		this.set_primary_action();
		this.set_actions_menu_items();
	}

	set_actions_menu_items() {
		this.actions_menu_items = this.get_actions_menu_items();
		this.workflow_action_menu_items = this.get_workflow_action_menu_items();
		this.workflow_action_items = {};

		const actions = this.actions_menu_items.concat(this.workflow_action_menu_items);
		actions.forEach((item) => {
			const $item = this.page.add_actions_menu_item(item.label, item.action, item.standard);
			if (item.class) {
				$item.addClass(item.class);
			}
			if (item.is_workflow_action && $item) {
				// can be used to dynamically show or hide action
				this.workflow_action_items[item.name] = $item;
			}
		});
	}

	show_restricted_list_indicator_if_applicable() {
		const match_rules_list = frappe.perm.get_match_rules(this.doctype);
		if (match_rules_list.length) {
			this.restricted_list = $(
				`<button class="btn btn-xs restricted-button flex align-center">
					${frappe.utils.icon("restriction", "xs")}
				</button>`
			)
				.click(() => this.show_restrictions(match_rules_list))
				.appendTo(this.page.page_form);
		}
	}

	show_restrictions(match_rules_list = []) {
		frappe.msgprint(
			frappe.render_template("list_view_permission_restrictions", {
				condition_list: match_rules_list,
			}),
			__("Restrictions", null, "Title of message showing restrictions in list view")
		);
	}

	get_fields() {
		return super
			.get_fields()
			.concat(
				Object.entries(this.link_field_title_fields || {}).map(
					(entry) => entry.join(".") + " as " + entry.join("_")
				)
			);
	}

	async set_fields() {
		this.link_field_title_fields = {};
		let fields = [].concat(
			frappe.model.std_fields_list,
			this.get_fields_in_list_view(),
			[this.meta.title_field, this.meta.image_field],
			this.settings.add_fields || [],
			this.meta.track_seen ? "_seen" : null,
			this.sort_by,
			"enabled",
			"disabled",
			"color"
		);

		await Promise.all(
			fields.map((f) => {
				return new Promise((resolve) => {
					const df =
						typeof f === "string" ? frappe.meta.get_docfield(this.doctype, f) : f;
					if (
						df &&
						df.fieldtype == "Link" &&
						frappe.boot.link_title_doctypes.includes(df.options)
					) {
						frappe.model.with_doctype(df.options, () => {
							const meta = frappe.get_meta(df.options);
							if (meta.show_title_field_in_link) {
								this.link_field_title_fields[
									typeof f === "string" ? f : f.fieldname
								] = meta.title_field;
							}

							this._add_field(f);
							resolve();
						});
					} else {
						this._add_field(f);
						resolve();
					}
				});
			})
		);

		this.fields.forEach((f) => {
			const df = frappe.meta.get_docfield(f[1], f[0]);
			if (df && df.fieldtype === "Currency" && df.options && !df.options.includes(":")) {
				this._add_field(df.options);
			}
		});
	}

	patch_refresh_and_load_lib() {
		// throttle refresh for 1s
		this.refresh = this.refresh.bind(this);
		this.refresh = frappe.utils.throttle(this.refresh, 1000);
		this.load_lib = new Promise((resolve) => {
			if (this.required_libs) {
				frappe.require(this.required_libs, resolve);
			} else {
				resolve();
			}
		});
		// call refresh every 5 minutes
		const interval = 5 * 60 * 1000;
		setInterval(() => {
			// don't call if route is different
			if (frappe.get_route_str() === this.page_name) {
				this.refresh();
			}
		}, interval);
	}

	set_primary_action() {
		if (this.can_create && !frappe.boot.read_only) {
			const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype);

			// Better style would be __("Add {0}", [doctype_name], "Primary action in list view")
			// Keeping it like this to not disrupt existing translations
			const label = `${__("Add", null, "Primary action in list view")} ${doctype_name}`;
			this.page.set_primary_action(
				label,
				() => {
					if (this.settings.primary_action) {
						this.settings.primary_action();
					} else {
						this.make_new_doc();
					}
				},
				"add"
			);
		} else {
			this.page.clear_primary_action();
		}
	}

	make_new_doc() {
		const doctype = this.doctype;
		const options = {};
		this.filter_area.get().forEach((f) => {
			if (f[2] === "=" && frappe.model.is_non_std_field(f[1])) {
				options[f[1]] = f[3];
			}
		});
		frappe.new_doc(doctype, options);
	}

	setup_view() {
		this.setup_columns();
		this.render_header();
		this.render_skeleton();
		this.setup_events();
		this.settings.onload && this.settings.onload(this);
		this.show_restricted_list_indicator_if_applicable();
	}

	refresh_columns(meta, list_view_settings) {
		this.meta = meta;
		this.list_view_settings = list_view_settings;

		this.setup_columns();
		this.refresh(true);
	}

	refresh(refresh_header = false) {
		return super.refresh().then(() => {
			this.render_header(refresh_header);
			this.update_checkbox();
			this.update_url_with_filters();
			this.setup_realtime_updates();
		});
	}

	update_checkbox(target) {
		if (!this.$checkbox_actions) return;

		let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");

		if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {
			$check_all_checkbox.prop("checked", false);
		}

		$check_all_checkbox.prop("checked", this.$checks.length === this.data.length);
	}

	setup_freeze_area() {
		this.$freeze = $(
			`<div class="freeze flex justify-center align-center text-muted">
				${__("Loading")}...
			</div>`
		).hide();
		this.$result.append(this.$freeze);
	}

	setup_columns() {
		// setup columns for list view
		this.columns = [];

		const get_df = frappe.meta.get_docfield.bind(null, this.doctype);

		// 1st column: title_field or name
		if (this.meta.title_field) {
			this.columns.push({
				type: "Subject",
				df: get_df(this.meta.title_field),
			});
		} else {
			this.columns.push({
				type: "Subject",
				df: {
					label: __("ID"),
					fieldname: "name",
				},
			});
		}

		this.columns.push({
			type: "Tag",
		});

		// 2nd column: Status indicator
		if (frappe.has_indicator(this.doctype)) {
			// indicator
			this.columns.push({
				type: "Status",
			});
		}

		const fields_in_list_view = this.get_fields_in_list_view();
		// Add rest from in_list_view docfields
		this.columns = this.columns.concat(
			fields_in_list_view
				.filter((df) => {
					if (frappe.has_indicator(this.doctype) && df.fieldname === "status") {
						return false;
					}
					if (!df.in_list_view || df.is_virtual) {
						return false;
					}
					return df.fieldname !== this.meta.title_field;
				})
				.map((df) => ({
					type: "Field",
					df,
				}))
		);

		if (this.list_view_settings.fields) {
			this.columns = this.reorder_listview_fields();
		}

		// limit max to 8 columns if no total_fields is set in List View Settings
		// Screen with low density no of columns 4
		// Screen with medium density no of columns 6
		// Screen with high density no of columns 8
		let total_fields = 6;

		if (window.innerWidth <= 1366) {
			total_fields = 4;
		} else if (window.innerWidth >= 1920) {
			total_fields = 10;
		}

		this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields);

		if (
			!this.settings.hide_name_column &&
			this.meta.title_field &&
			this.meta.title_field !== "name"
		) {
			this.columns.push({
				type: "Field",
				df: {
					label: __("ID"),
					fieldname: "name",
				},
			});
		}
	}

	reorder_listview_fields() {
		let fields_order = [];
		let fields = JSON.parse(this.list_view_settings.fields);

		//title and tags field is fixed
		fields_order.push(this.columns[0]);
		fields_order.push(this.columns[1]);
		this.columns.splice(0, 2);

		for (let fld in fields) {
			for (let col in this.columns) {
				let field = fields[fld];
				let column = this.columns[col];

				if (column.type == "Status" && field.fieldname == "status_field") {
					fields_order.push(column);
					break;
				} else if (column.type == "Field" && field.fieldname === column.df.fieldname) {
					fields_order.push(column);
					break;
				}
			}
		}

		return fields_order;
	}

	get_documentation_link() {
		if (this.meta.documentation) {
			return `<a href="${this.meta.documentation}" target="blank" class="meta-description small text-muted">Need Help?</a>`;
		}
		return "";
	}

	get_no_result_message() {
		let help_link = this.get_documentation_link();
		let filters = this.filter_area && this.filter_area.get();

		let has_filters_set = filters && filters.length;
		let no_result_message = has_filters_set
			? __("No {0} found with matching filters. Clear filters to see all {0}.", [
					__(this.doctype),
			  ])
			: this.meta.description
			? __(this.meta.description)
			: __("You haven't created a {0} yet", [__(this.doctype)]);

		let new_button_label = has_filters_set
			? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view")
			: __(
					"Create your first {0}",
					[__(this.doctype)],
					"Create a new document from list view"
			  );
		let empty_state_image =
			this.settings.empty_state_image ||
			"/assets/frappe/images/ui-states/list-empty-state.svg";

		const new_button = this.can_create
			? `<p><button class="btn btn-default btn-sm btn-new-doc hidden-xs">
				${new_button_label}
			</button> <button class="btn btn-primary btn-new-doc visible-xs">
				${__("Create New", null, "Create a new document from list view")}
			</button></p>`
			: "";

		return `<div class="msg-box no-border">
			<div>
				<img src="${empty_state_image}" alt="Generic Empty State" class="null-state">
			</div>
			<p>${no_result_message}</p>
			${new_button}
			${help_link}
		</div>`;
	}

	freeze() {
		if (this.list_view_settings && !this.list_view_settings.disable_count) {
			this.get_count_element().html(
				`<span>${__("Refreshing", null, "Document count in list view")}...</span>`
			);
		}
	}

	get_args() {
		const args = super.get_args();

		if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
			args.with_comment_count = 1;
		} else {
			args.with_comment_count = 0;
		}

		return args;
	}

	before_refresh() {
		if (frappe.route_options && this.filter_area) {
			this.filters = this.parse_filters_from_route_options();
			frappe.route_options = null;

			if (this.filters.length > 0) {
				return this.filter_area
					.clear(false)
					.then(() => this.filter_area.set(this.filters));
			}
		}

		return Promise.resolve();
	}

	parse_filters_from_settings() {
		return (this.settings.filters || []).map((f) => {
			if (f.length === 3) {
				f = [this.doctype, f[0], f[1], f[2]];
			}
			return f;
		});
	}

	toggle_result_area() {
		super.toggle_result_area();
		this.toggle_actions_menu_button(this.$result.find(".list-row-check:checked").length > 0);
	}

	toggle_actions_menu_button(toggle) {
		if (toggle) {
			this.page.show_actions_menu();
			this.page.clear_primary_action();
		} else {
			this.page.hide_actions_menu();
			this.set_primary_action();
		}
	}

	render_header(refresh_header = false) {
		if (refresh_header) {
			this.$result.find(".list-row-head").remove();
		}
		if (this.$result.find(".list-row-head").length === 0) {
			// append header once
			this.$result.prepend(this.get_header_html());
		}
	}

	render_skeleton() {
		const $row = this.get_list_row_html_skeleton(
			'<div><input type="checkbox" class="render-list-checkbox"/></div>'
		);
		this.$result.append($row);
	}

	before_render() {
		this.settings.before_render && this.settings.before_render();
		frappe.model.user_settings.save(this.doctype, "last_view", this.view_name);
		this.save_view_user_settings({
			filters: this.filter_area && this.filter_area.get(),
			sort_by: this.sort_selector && this.sort_selector.sort_by,
			sort_order: this.sort_selector && this.sort_selector.sort_order,
		});
		this.toggle_paging && this.$paging_area.toggle(false);
	}

	after_render() {
		this.$no_result.html(`
			<div class="no-result text-muted flex justify-center align-center">
				${this.get_no_result_message()}
			</div>
		`);
		this.setup_new_doc_event();
		this.toggle_paging && this.$paging_area.toggle(true);
	}

	render() {
		this.render_list();
		this.set_rows_as_checked();
		this.render_count();
	}

	render_list() {
		// clear rows
		this.$result.find(".list-row-container").remove();

		if (this.data.length > 0) {
			// append rows
			let idx = 0;
			for (let doc of this.data) {
				doc._idx = idx++;
				this.$result.append(this.get_list_row_html(doc));
			}
		}
	}

	render_count() {
		if (this.list_view_settings.disable_count) return;

		let me = this;
		let $count = this.get_count_element();
		this.get_count_str().then((count) => {
			$count.html(`<span>${count}</span>`);
			if (this.count_upper_bound && this.count_upper_bound == this.total_count) {
				$count.attr(
					"title",
					__(
						"The count shown is an estimated count. Click here to see the accurate count."
					)
				);
				$count.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" });
				$count.on("click", () => {
					me.count_upper_bound = 0;
					$count.off("click");
					$count.tooltip("disable");
					me.freeze();
					me.render_count();
				});
			}
		});
	}

	get_count_element() {
		return this.$result.find(".list-count");
	}

	get_header_html() {
		if (!this.columns) {
			return;
		}

		const subject_field = this.columns[0].df;
		let subject_html = `
			<input class="level-item list-check-all" type="checkbox"
				title="${__("Select All")}">
			<span class="level-item" data-sort-by="${subject_field.fieldname}"
				title="${__("Click to sort by {0}", [subject_field.label])}">
				${__(subject_field.label)}
			</span>
		`;
		const $columns = this.columns
			.map((col) => {
				let classes = [
					"list-row-col ellipsis",
					col.type == "Subject" ? "list-subject level" : "hidden-xs",
					col.type == "Tag" ? "tag-col hide" : "",
					frappe.model.is_numeric_field(col.df) ? "text-right" : "",
				].join(" ");

				let html = "";
				if (col.type === "Subject") {
					html = subject_html;
				} else {
					const fieldname = col.df?.fieldname;
					const label = __(col.df?.label || col.type, null, col.df?.parent);
					const title = __("Click to sort by {0}", [label]);
					const attrs = fieldname ? `data-sort-by="${fieldname}" title="${title}"` : "";
					html = `<span ${attrs}>${label}</span>`;
				}

				return `<div class="${classes}">${html}</div>
			`;
			})
			.join("");

		const right_html = `
			<span class="list-count"></span>
			<span class="level-item list-liked-by-me hidden-xs">
				<span title="${__("Liked by me")}">
					${frappe.utils.icon("es-solid-heart", "sm", "like-icon")}
				</span>
			</span>
		`;

		return this.get_header_html_skeleton($columns, right_html);
	}

	get_header_html_skeleton(left = "", right = "") {
		return `
			<header class="level list-row-head text-muted">
				<div class="level-left list-header-subject">
					${left}
				</div>
				<div class="level-left checkbox-actions">
					<div class="level list-subject">
						<input class="level-item list-check-all" type="checkbox"
							title="${__("Select All")}">
						<span class="level-item list-header-meta"></span>
					</div>
				</div>
				<div class="level-right">
					${right}
				</div>
			</header>
		`;
	}

	get_left_html(doc) {
		return this.columns.map((col) => this.get_column_html(col, doc)).join("");
	}

	get_right_html(doc) {
		return this.get_meta_html(doc);
	}

	get_list_row_html(doc) {
		return this.get_list_row_html_skeleton(this.get_left_html(doc), this.get_right_html(doc));
	}

	get_list_row_html_skeleton(left = "", right = "") {
		return `
			<div class="list-row-container" tabindex="1">
				<div class="level list-row">
					<div class="level-left ellipsis">
						${left}
					</div>
					<div class="level-right text-muted ellipsis">
						${right}
					</div>
				</div>
				<div class="list-row-border"></div>
			</div>
		`;
	}

	get_column_html(col, doc) {
		if (col.type === "Status" || col.df?.options == "Workflow State") {
			let show_workflow_state = col.df?.options == "Workflow State";
			return `
				<div class="list-row-col hidden-xs ellipsis">
					${this.get_indicator_html(doc, show_workflow_state)}
				</div>
			`;
		}

		if (col.type === "Tag") {
			const tags_display_class = !this.tags_shown ? "hide" : "";
			let tags_html = doc._user_tags
				? this.get_tags_html(doc._user_tags, 2, true)
				: '<div class="tags-empty">-</div>';
			return `
				<div class="list-row-col tag-col ${tags_display_class} hidden-xs ellipsis">
					${tags_html}
				</div>
			`;
		}

		const df = col.df || {};
		const label = df.label;
		const fieldname = df.fieldname;
		const link_title_fieldname = this.link_field_title_fields[fieldname];
		const value = doc[fieldname] || "";
		let value_display = link_title_fieldname
			? doc[fieldname + "_" + link_title_fieldname] || value
			: value;

		let translated_doctypes = frappe.boot?.translated_doctypes || [];
		if (translated_doctypes.includes(df.options)) {
			value_display = __(value_display);
		}

		const format = () => {
			if (df.fieldtype === "Code") {
				return value;
			} else if (df.fieldtype === "Percent") {
				return `<div class="progress" style="margin: 0px;">
						<div class="progress-bar progress-bar-success" role="progressbar"
							aria-valuenow="${value}"
							aria-valuemin="0" aria-valuemax="100" style="width: ${Math.round(value)}%;">
						</div>
					</div>`;
			} else {
				return frappe.format(value, df, null, doc);
			}
		};

		const field_html = () => {
			let html;
			let _value;
			let strip_html_required =
				df.fieldtype == "Text Editor" ||
				(df.fetch_from && ["Text", "Small Text"].includes(df.fieldtype));

			if (strip_html_required) {
				_value = strip_html(value_display);
			} else {
				_value =
					typeof value_display === "string"
						? frappe.utils.escape_html(value_display)
						: value_display;
			}

			if (df.fieldtype === "Rating") {
				let out_of_ratings = df.options || 5;
				_value = _value * out_of_ratings;
			}

			if (df.fieldtype === "Image") {
				html = df.options
					? `<img src="${doc[df.options]}"
					style="max-height: 30px; max-width: 100%;">`
					: `<div class="missing-image small">
						${frappe.utils.icon("restriction")}
					</div>`;
			} else if (df.fieldtype === "Select") {
				html = `<span class="filterable indicator-pill ${frappe.utils.guess_colour(
					_value
				)} ellipsis"
					data-filter="${fieldname},=,${value}">
					<span class="ellipsis"> ${__(_value)} </span>
				</span>`;
			} else if (df.fieldtype === "Link") {
				html = `<a class="filterable ellipsis"
					data-filter="${fieldname},=,${value}">
					${_value}
				</a>`;
			} else if (
				["Text Editor", "Text", "Small Text", "HTML Editor", "Markdown Editor"].includes(
					df.fieldtype
				)
			) {
				html = `<span class="ellipsis">
					${_value}
				</span>`;
			} else {
				html = `<a class="filterable ellipsis"
					data-filter="${fieldname},=,${frappe.utils.escape_html(value)}">
					${format()}
				</a>`;
			}

			return `<span class="ellipsis"
				title="${__(label)}: ${frappe.utils.escape_html(_value)}">
				${html}
			</span>`;
		};

		const class_map = {
			Subject: "list-subject level",
			Field: "hidden-xs",
		};
		const css_class = [
			"list-row-col ellipsis",
			class_map[col.type],
			frappe.model.is_numeric_field(df) ? "text-right" : "",
		].join(" ");

		let column_html;
		if (
			this.settings.formatters &&
			this.settings.formatters[fieldname] &&
			col.type !== "Subject"
		) {
			column_html = this.settings.formatters[fieldname](value, df, doc);
		} else {
			column_html = {
				Subject: this.get_subject_element(doc, value_display).innerHTML,
				Field: field_html(),
			}[col.type];
		}

		return `
			<div class="${css_class}">
				${column_html}
			</div>
		`;
	}

	get_tags_html(user_tags, limit, colored = false) {
		let get_tag_html = (tag) => {
			let color = "",
				style = "";
			if (tag) {
				if (colored) {
					color = frappe.get_palette(tag);
					style = `background-color: var(${color[0]}); color: var(${color[1]})`;
				}

				return `<div class="tag-pill ellipsis" title="${tag}" style="${style}">${tag}</div>`;
			}
		};
		return user_tags
			.split(",")
			.slice(1, limit + 1)
			.map(get_tag_html)
			.join("");
	}

	get_meta_html(doc) {
		let html = "";

		let settings_button = null;
		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 = ``;

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

		let comment_count = null;
		if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
			comment_count = `<span class="comment-count d-flex align-items-center">
				${frappe.utils.icon("es-line-chat-alt")}
				${doc._comment_count > 99 ? "99+" : doc._comment_count || 0}
			</span>`;
		}

		html += `
			<div class="level-item list-row-activity hidden-xs">
				<div class="hidden-md hidden-xs">
					${settings_button || assigned_to}
				</div>
				<span class="modified">${modified}</span>
				${comment_count || ""}
				${comment_count ? '<span class="mx-2">·</span>' : ""}
				<span class="list-row-like hidden-xs style="margin-bottom: 1px;">
					${this.get_like_html(doc)}
				</span>
			</div>
			<div class="level-item visible-xs text-right">
				${this.get_indicator_html(doc)}
			</div>
		`;

		return html;
	}

	get_count_str() {
		let current_count = this.data.length;
		let count_without_children = this.data.uniqBy((d) => d.name).length;

		return frappe.db
			.count(this.doctype, {
				filters: this.get_filters_for_args(),
				limit: this.count_upper_bound,
			})
			.then((total_count) => {
				this.total_count = total_count || current_count;
				this.count_without_children =
					count_without_children !== current_count ? count_without_children : undefined;

				let count_str;
				if (this.total_count === this.count_upper_bound) {
					count_str = `${format_number(this.total_count - 1, null, 0)}+`;
				} else {
					count_str = format_number(this.total_count, null, 0);
				}

				let str = __("{0} of {1}", [format_number(current_count, null, 0), count_str]);
				if (this.count_without_children) {
					str = __("{0} of {1} ({2} rows with children)", [
						count_without_children,
						count_str,
						current_count,
					]);
				}
				return str;
			});
	}

	get_form_link(doc) {
		if (this.settings.get_form_link) {
			return this.settings.get_form_link(doc);
		}

		return `/app/${encodeURIComponent(
			frappe.router.slug(frappe.router.doctype_layout || this.doctype)
		)}/${encodeURIComponent(cstr(doc.name))}`;
	}

	get_seen_class(doc) {
		const seen_by = doc._seen ? JSON.parse(doc._seen) : [];
		return seen_by.includes(frappe.session.user) ? "" : "bold";
	}

	get_like_html(doc) {
		const liked_by = doc._liked_by ? JSON.parse(doc._liked_by) : [];
		const is_liked = liked_by.includes(frappe.session.user);
		const title = liked_by.map((u) => frappe.user_info(u).fullname).join(", ");

		const div = document.createElement("div");
		div.appendChild(
			this._element_factory.get_like_element(doc.name, is_liked, liked_by, title)
		);

		return div.innerHTML;
	}

	get_subject_element(doc, title) {
		const ef = this._element_factory;
		const div = document.createElement("div");
		const checkboxspan = ef.get_checkboxspan_element();

		const ellipsisSpan = document.createElement("span");
		const seen = this.get_seen_class(doc);
		if (seen) {
			ellipsisSpan.classList.add("level-item", seen, "ellipsis");
		}

		div.appendChild(checkboxspan).appendChild(ef.get_checkbox_element(doc.name));
		div.appendChild(ellipsisSpan).appendChild(
			ef.get_link_element(
				doc.name,
				this.get_form_link(doc),
				this.get_subject_text(doc, title)
			)
		);

		return div;
	}

	get_subject_text(doc, title) {
		const subject_field = this.columns[0].df;
		let value = title || doc[subject_field.fieldname];
		if (this.settings.formatters && this.settings.formatters[subject_field.fieldname]) {
			let formatter = this.settings.formatters[subject_field.fieldname];
			value = formatter(value, subject_field, doc);
		}

		if (!value) {
			value = doc.name;
		}

		if (frappe.model.html_fieldtypes.includes(subject_field.fieldtype)) {
			// NOTE: this is very slow, so only do it for HTML fields
			return frappe.utils.html2text(value);
		} else {
			return value;
		}
	}

	get_indicator_html(doc, show_workflow_state) {
		const indicator = frappe.get_indicator(doc, this.doctype, show_workflow_state);
		// sequence is important
		const docstatus_description = [
			__("Document is in draft state"),
			__("Document has been submitted"),
			__("Document has been cancelled"),
		];
		const title = docstatus_description[doc.docstatus || 0];
		if (indicator) {
			return `<span class="indicator-pill ${
				indicator[1]
			} filterable no-indicator-dot ellipsis"
				data-filter='${indicator[2]}' title='${title}'>
				<span class="ellipsis"> ${__(indicator[0])}</span>
			</span>`;
		}
		return "";
	}

	get_indicator_dot(doc) {
		const indicator = frappe.get_indicator(doc, this.doctype);
		if (!indicator) return "";
		return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`;
	}

	get_image_url(doc) {
		let url = doc.image ? doc.image : doc[this.meta.image_field];
		// absolute url for mobile
		if (window.cordova && !frappe.utils.is_url(url)) {
			url = frappe.base_url + url;
		}
		return url || null;
	}

	setup_events() {
		this.setup_filterable();
		this.setup_sort_by();
		this.setup_list_click();
		this.setup_drag_click();
		this.setup_tag_event();
		this.setup_new_doc_event();
		this.setup_check_events();
		this.setup_like();
		this.setup_realtime_updates();
		this.setup_action_handler();
		this.setup_keyboard_navigation();
	}

	setup_keyboard_navigation() {
		let focus_first_row = () => {
			this.$result.find(".list-row-container:first").focus();
		};
		let focus_next = () => {
			$(document.activeElement).next().focus();
		};
		let focus_prev = () => {
			$(document.activeElement).prev().focus();
		};
		let list_row_focused = () => {
			return $(document.activeElement).is(".list-row-container");
		};
		let check_row = ($row) => {
			let $input = $row.find("input[type=checkbox]");
			$input.click();
		};
		let get_list_row_if_focused = () =>
			list_row_focused() ? $(document.activeElement) : null;

		let is_current_page = () => this.page.wrapper.is(":visible");
		let is_input_focused = () => $(document.activeElement).is("input");

		let handle_navigation = (direction) => {
			if (!is_current_page() || is_input_focused()) return false;

			let $list_row = get_list_row_if_focused();
			if ($list_row) {
				direction === "down" ? focus_next() : focus_prev();
			} else {
				focus_first_row();
			}
		};

		frappe.ui.keys.add_shortcut({
			shortcut: "down",
			action: () => handle_navigation("down"),
			description: __("Navigate list down", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "up",
			action: () => handle_navigation("up"),
			description: __("Navigate list up", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "shift+down",
			action: () => {
				if (!is_current_page() || is_input_focused()) return false;
				let $list_row = get_list_row_if_focused();
				check_row($list_row);
				focus_next();
			},
			description: __(
				"Select multiple list items",
				null,
				"Description of a list view shortcut"
			),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "shift+up",
			action: () => {
				if (!is_current_page() || is_input_focused()) return false;
				let $list_row = get_list_row_if_focused();
				check_row($list_row);
				focus_prev();
			},
			description: __(
				"Select multiple list items",
				null,
				"Description of a list view shortcut"
			),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "enter",
			action: () => {
				let $list_row = get_list_row_if_focused();
				if ($list_row) {
					$list_row.find("a[data-name]")[0].click();
					return true;
				}
				return false;
			},
			description: __("Open list item", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "space",
			action: () => {
				let $list_row = get_list_row_if_focused();
				if ($list_row) {
					check_row($list_row);
					return true;
				}
				return false;
			},
			description: __("Select list item", null, "Description of a list view shortcut"),
			page: this.page,
		});
	}

	setup_filterable() {
		// filterable events
		this.$result.on("click", ".filterable", (e) => {
			if (e.metaKey || e.ctrlKey) return;
			e.stopPropagation();
			const $this = $(e.currentTarget);
			const filters = $this.attr("data-filter").split("|");
			const filters_to_apply = filters.map((f) => {
				f = f.split(",");
				if (f[2] === "Today") {
					f[2] = frappe.datetime.get_today();
				} else if (f[2] == "User") {
					f[2] = frappe.session.user;
				}
				this.filter_area.remove(f[0]);
				return [this.doctype, f[0], f[1], f.slice(2).join(",")];
			});
			this.filter_area.add(filters_to_apply);
		});
	}

	setup_sort_by() {
		this.$result.on("click", "[data-sort-by]", (e) => {
			const sort_by = e.currentTarget.getAttribute("data-sort-by");
			if (!sort_by) return;
			let sort_order = "asc"; // always start with asc
			if (this.sort_by === sort_by) {
				// unless it's the same field, then toggle
				sort_order = this.sort_order === "asc" ? "desc" : "asc";
			}
			this.sort_selector.set_value(sort_by, sort_order);
			this.on_sort_change(sort_by, sort_order);
		});
	}

	setup_list_click() {
		this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
			const $target = $(e.target);
			// tick checkbox if Ctrl/Meta key is pressed
			if ((e.ctrlKey || e.metaKey) && !$target.is("a")) {
				const $list_row = $(e.currentTarget);
				const $check = $list_row.find(".list-row-checkbox");
				$check.prop("checked", !$check.prop("checked"));
				e.preventDefault();
				this.on_row_checked();
				return;
			}
			// don't open form when checkbox, like, filterable are clicked
			if (
				$target.hasClass("filterable") ||
				$target.hasClass("select-like") ||
				$target.hasClass("file-select") ||
				$target.hasClass("list-row-like") ||
				$target.is(":checkbox")
			) {
				e.stopPropagation();
				return;
			}

			// link, let the event be handled via set_route
			if ($target.is("a")) return;

			// clicked on the row, open form
			const $row = $(e.currentTarget);
			const link = $row.find(".list-subject a").get(0);
			if (link) {
				frappe.set_route(link.pathname);
				return false;
			}
		});
	}

	setup_drag_click() {
		/*
			Click on the check box in the list view and
			drag through the rows to select.

			Do it again to unselect.

			If the first click is on checked checkbox, then it will unselect rows on drag,
			else if it is unchecked checkbox, it will select rows on drag.
		*/
		this.dragClick = false;
		this.$result.on("mousedown", ".list-row-checkbox", (e) => {
			e.stopPropagation?.();
			e.preventDefault?.();
			this.dragClick = true;
			this.check = !e.target.checked;
		});
		$(document).on("mouseup", () => {
			this.dragClick = false;
		});
		this.$result.on("mousemove", ".level.list-row", (e) => {
			if (this.dragClick) {
				this.check_row_on_drag(e, this.check);
			}
		});
	}

	check_row_on_drag(event, check = true) {
		$(event.target).find(".list-row-checkbox").prop("checked", check);
		this.on_row_checked();
	}

	setup_action_handler() {
		this.$result.on("click", ".btn-action", (e) => {
			const $button = $(e.currentTarget);
			const doc = this.data[$button.attr("data-idx")];
			this.settings.button.action(doc);
			e.stopPropagation();
			return false;
		});
	}

	setup_check_events() {
		this.$result.on("change", "input[type=checkbox]", (e) => {
			const $target = $(e.currentTarget);

			if ($target.is(".list-header-subject .list-check-all")) {
				const $check = this.$result.find(".checkbox-actions .list-check-all");
				$check.prop("checked", $target.prop("checked"));
				$check.trigger("change");
			} else if ($target.is(".checkbox-actions .list-check-all")) {
				const $check = this.$result.find(".list-header-subject .list-check-all");
				$check.prop("checked", $target.prop("checked"));

				this.$result.find(".list-row-checkbox").prop("checked", $target.prop("checked"));
			} else if ($target.attr("data-parent")) {
				this.$result
					.find(`.${$target.attr("data-parent")}`)
					.find(".list-row-checkbox")
					.prop("checked", $target.prop("checked"));
			}

			this.on_row_checked();
		});

		this.$result.on("click", ".list-row-checkbox", (e) => {
			const $target = $(e.currentTarget);

			// shift select checkboxes
			if (e.shiftKey && this.$checkbox_cursor && !$target.is(this.$checkbox_cursor)) {
				const name_1 = decodeURIComponent(this.$checkbox_cursor.data().name);
				const name_2 = decodeURIComponent($target.data().name);
				const index_1 = this.data.findIndex((d) => d.name === name_1);
				const index_2 = this.data.findIndex((d) => d.name === name_2);
				let [min_index, max_index] = [index_1, index_2];

				if (min_index > max_index) {
					[min_index, max_index] = [max_index, min_index];
				}

				let docnames = this.data.slice(min_index + 1, max_index).map((d) => d.name);
				const selector = docnames
					.map((name) => `.list-row-checkbox[data-name="${encodeURIComponent(name)}"]`)
					.join(",");
				this.$result.find(selector).prop("checked", true);
			}

			this.$checkbox_cursor = $target;

			this.update_checkbox($target);
		});

		let me = this;
		this.page.actions_btn_group.on("show.bs.dropdown", () => {
			me.toggle_workflow_actions();
		});
	}

	setup_like() {
		this.$result.on("click", ".like-action", (e) => {
			const $this = $(e.currentTarget);
			const { doctype, name } = $this.data();
			frappe.ui.toggle_like($this, doctype, name);

			return false;
		});

		this.$result.on("click", ".list-liked-by-me", (e) => {
			const $this = $(e.currentTarget);
			$this.toggleClass("active");

			if ($this.hasClass("active")) {
				this.filter_area.add(
					this.doctype,
					"_liked_by",
					"like",
					"%" + frappe.session.user + "%"
				);
			} else {
				this.filter_area.remove("_liked_by");
			}
		});
	}

	setup_new_doc_event() {
		this.$no_result.find(".btn-new-doc").click(() => {
			if (this.settings.primary_action) {
				this.settings.primary_action();
			} else {
				this.make_new_doc();
			}
		});
	}

	setup_tag_event() {
		this.tags_shown = false;
		this.list_sidebar &&
			this.list_sidebar.parent.on("click", ".list-tag-preview", () => {
				this.tags_shown = !this.tags_shown;
				this.toggle_tags();
			});
	}

	setup_realtime_updates() {
		this.pending_document_refreshes = [];

		if (this.list_view_settings?.disable_auto_refresh || this.realtime_events_setup) {
			return;
		}
		frappe.realtime.doctype_subscribe(this.doctype);
		frappe.realtime.off("list_update");
		frappe.realtime.on("list_update", (data) => {
			if (data?.doctype !== this.doctype) {
				return;
			}

			// if some bulk operation is happening by selecting list items, don't refresh
			if (this.$checks && this.$checks.length) {
				return;
			}

			if (this.avoid_realtime_update()) {
				return;
			}

			this.pending_document_refreshes.push(data);
			this.debounced_refresh();
		});
		this.realtime_events_setup = true;
	}

	disable_realtime_updates() {
		frappe.realtime.doctype_unsubscribe(this.doctype);
		this.realtime_events_setup = false;
	}

	process_document_refreshes() {
		if (!this.pending_document_refreshes.length) return;

		const route = frappe.get_route() || [];
		if (!cur_list || route[0] != "List" || cur_list.doctype != route[1]) {
			// wait till user is back on list view before refreshing
			this.pending_document_refreshes = [];
			this.disable_realtime_updates();
			return;
		}

		const names = this.pending_document_refreshes.map((d) => d.name);
		this.pending_document_refreshes = this.pending_document_refreshes.filter(
			(d) => names.indexOf(d.name) === -1
		);

		if (!names.length) return;

		// filters to get only the doc with this name
		const call_args = this.get_call_args();
		call_args.args.filters.push([this.doctype, "name", "in", names]);
		call_args.args.start = 0;

		frappe.call(call_args).then(({ message }) => {
			if (!message) return;
			const data = frappe.utils.dict(message.keys, message.values);

			if (!(data && data.length)) {
				// this doc was changed and should not be visible
				// in the listview according to filters applied
				// let's remove it manually
				this.data = this.data.filter((d) => !names.includes(d.name));
				for (let name of names) {
					this.$result
						.find(`.list-row-checkbox[data-name='${name.replace(/'/g, "\\'")}']`)
						.closest(".list-row-container")
						.remove();
				}
				return;
			}

			data.forEach((datum) => {
				const index = this.data.findIndex((doc) => doc.name === datum.name);

				if (index === -1) {
					// append new data
					this.data.push(datum);
				} else {
					// update this data in place
					this.data[index] = datum;
				}
			});

			this.data.sort((a, b) => {
				const a_value = a[this.sort_by] || "";
				const b_value = b[this.sort_by] || "";

				let return_value = 0;
				if (a_value > b_value) {
					return_value = 1;
				}

				if (b_value > a_value) {
					return_value = -1;
				}

				if (this.sort_order === "desc") {
					return_value = -return_value;
				}
				return return_value;
			});
			if (this.$checks && this.$checks.length) {
				this.set_rows_as_checked();
			}
			this.toggle_result_area();
			this.render_list();
		});
	}

	avoid_realtime_update() {
		if (this.filter_area?.is_being_edited()) {
			return true;
		}
		// this is set when a bulk operation is called from a list view which might update the list view
		// this is to avoid the list view from refreshing a lot of times
		// the list view is updated once after the bulk operation is complete
		if (this.disable_list_update) {
			return true;
		}
		return false;
	}

	set_rows_as_checked() {
		if (!this.$checks || !this.$checks.length) {
			return;
		}

		$.each(this.$checks, (i, el) => {
			let docname = $(el).attr("data-name");
			this.$result.find(`.list-row-checkbox[data-name='${docname}']`).prop("checked", true);
		});
		this.on_row_checked();
	}

	on_row_checked() {
		this.$list_head_subject =
			this.$list_head_subject || this.$result.find("header .list-header-subject");
		this.$checkbox_actions =
			this.$checkbox_actions || this.$result.find("header .checkbox-actions");

		this.$checks = this.$result.find(".list-row-checkbox:checked");

		this.$list_head_subject.toggle(this.$checks.length === 0);
		this.$checkbox_actions.toggle(this.$checks.length > 0);

		if (this.$checks.length === 0) {
			this.$list_head_subject.find(".list-check-all").prop("checked", false);
		} else {
			this.$checkbox_actions
				.find(".list-header-meta")
				.html(__("{0} items selected", [this.$checks.length]));
			this.$checkbox_actions.show();
			this.$list_head_subject.hide();
		}
		this.update_checkbox();
		this.toggle_actions_menu_button(this.$checks.length > 0);
	}

	toggle_tags() {
		this.$result.find(".tag-col").toggleClass("hide");
		const preview_label = this.tags_shown ? __("Hide Tags") : __("Show Tags");
		this.list_sidebar.parent.find(".list-tag-preview").text(preview_label);
	}

	get_checked_items(only_docnames) {
		const docnames = Array.from(this.$checks || []).map((check) =>
			cstr(unescape($(check).data().name))
		);

		if (only_docnames) return docnames;

		return this.data.filter((d) => docnames.includes(d.name));
	}

	clear_checked_items() {
		this.$checks && this.$checks.prop("checked", false);
		this.on_row_checked();
	}

	save_view_user_settings(obj) {
		return frappe.model.user_settings.save(this.doctype, this.view_name, obj);
	}

	on_update() {}

	update_url_with_filters() {
		if (frappe.get_route_str() == this.page_name && !this.report_name) {
			// only update URL if the route still matches current page.
			// do not update if current list is a "saved report".
			window.history.replaceState(null, null, this.get_url_with_filters());
		}
	}

	get_url_with_filters() {
		let search_params = this.get_search_params();

		let full_url = window.location.href.replace(window.location.search, "");
		if (search_params.size) {
			full_url += "?" + search_params.toString();
		}
		return full_url;
	}

	get_search_params() {
		let search_params = new URLSearchParams();

		this.get_filters_for_args().forEach((filter) => {
			if (filter[2] === "=") {
				search_params.append(filter[1], filter[3]);
			} else {
				search_params.append(filter[1], JSON.stringify([filter[2], filter[3]]));
			}
		});
		return search_params;
	}

	get_menu_items() {
		const doctype = this.doctype;
		const items = [];

		if (frappe.model.can_import(doctype, null, this.meta)) {
			items.push({
				label: __("Import", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("list", "data-import", {
						reference_doctype: doctype,
					}),
				standard: true,
			});
		}

		if (frappe.user_roles.includes("System Manager")) {
			items.push({
				label: __("User Permissions", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("list", "user-permission", {
						allow: doctype,
					}),
				standard: true,
			});
		}

		if (frappe.user_roles.includes("System Manager")) {
			items.push({
				label: __("Role Permissions Manager", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("permission-manager", {
						doctype,
					}),
				standard: true,
			});
		}

		if (
			frappe.model.can_create("Custom Field") &&
			frappe.model.can_create("Property Setter")
		) {
			items.push({
				label: __("Customize", null, "Button in list view menu"),
				action: () => {
					if (!this.meta) return;
					if (this.meta.custom) {
						frappe.set_route("form", "doctype", doctype);
					} else if (!this.meta.custom) {
						frappe.set_route("form", "customize-form", {
							doc_type: doctype,
						});
					}
				},
				standard: true,
				shortcut: "Ctrl+J",
			});
		}

		items.push({
			label: __("Toggle Sidebar", null, "Button in list view menu"),
			action: () => this.toggle_side_bar(),
			condition: () => !this.hide_sidebar,
			standard: true,
			shortcut: "Ctrl+K",
		});

		if (frappe.user.has_role("System Manager") && frappe.boot.developer_mode === 1) {
			// edit doctype
			items.push({
				label: __("Edit DocType", null, "Button in list view menu"),
				action: () => frappe.set_route("form", "doctype", doctype),
				standard: true,
			});
		}

		if (frappe.user.has_role("System Manager")) {
			if (this.get_view_settings) {
				items.push(this.get_view_settings());
			}
		}

		return items;
	}

	get_view_settings() {
		return {
			label: __("List Settings", null, "Button in list view menu"),
			action: () => this.show_list_settings(),
			standard: true,
		};
	}

	show_list_settings() {
		frappe.model.with_doctype(this.doctype, () => {
			new ListSettings({
				listview: this,
				doctype: this.doctype,
				settings: this.list_view_settings,
				meta: frappe.get_meta(this.doctype),
			});
		});
	}

	get_workflow_action_menu_items() {
		const workflow_actions = [];
		const me = this;

		if (frappe.model.has_workflow(this.doctype)) {
			const actions = frappe.workflow.get_all_transition_actions(this.doctype);
			actions.forEach((action) => {
				workflow_actions.push({
					label: __(action),
					name: action,
					action: () => {
						me.disable_list_update = true;
						frappe
							.xcall("frappe.model.workflow.bulk_workflow_approval", {
								docnames: this.get_checked_items(true),
								doctype: this.doctype,
								action: action,
							})
							.finally(() => {
								me.disable_list_update = false;
							});
					},
					is_workflow_action: true,
				});
			});
		}
		return workflow_actions;
	}

	toggle_workflow_actions() {
		if (!frappe.model.has_workflow(this.doctype)) return;

		Object.keys(this.workflow_action_items).forEach((key) => {
			this.workflow_action_items[key].addClass("disabled");
		});
		const checked_items = this.get_checked_items();

		frappe
			.xcall("frappe.model.workflow.get_common_transition_actions", {
				docs: checked_items,
				doctype: this.doctype,
			})
			.then((actions) => {
				Object.keys(this.workflow_action_items).forEach((key) => {
					this.workflow_action_items[key].removeClass("disabled");
					this.workflow_action_items[key].toggle(actions.includes(key));
				});
			});
	}

	get_actions_menu_items() {
		const doctype = this.doctype;
		const actions_menu_items = [];
		const bulk_operations = new BulkOperations({ doctype: this.doctype });

		const is_field_editable = (field_doc) => {
			return (
				field_doc.fieldname &&
				frappe.model.is_value_type(field_doc) &&
				field_doc.fieldtype !== "Read Only" &&
				!field_doc.hidden &&
				!field_doc.read_only &&
				!field_doc.is_virtual
			);
		};

		const has_editable_fields = (doctype) => {
			return frappe.meta
				.get_docfields(doctype)
				.some((field_doc) => is_field_editable(field_doc));
		};

		const has_submit_permission = (doctype) => {
			return frappe.perm.has_perm(doctype, 0, "submit");
		};

		// utility
		const bulk_assignment = () => {
			return {
				label: __("Assign To", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.assign(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_assignment_rule = () => {
			return {
				label: __("Apply Assignment Rule", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.apply_assignment_rule(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_add_tags = () => {
			return {
				label: __("Add Tags", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.add_tags(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_printing = () => {
			return {
				label: __("Print", null, "Button in list view actions menu"),
				action: () => bulk_operations.print(this.get_checked_items()),
				standard: true,
			};
		};

		const bulk_delete = () => {
			return {
				label: __("Delete", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true).map((docname) =>
						docname.toString()
					);
					let message = __(
						"Delete {0} item permanently?",
						[docnames.length],
						"Title of confirmation dialog"
					);
					if (docnames.length > 1) {
						message = __(
							"Delete {0} items permanently?",
							[docnames.length],
							"Title of confirmation dialog"
						);
					}
					frappe.confirm(message, () => {
						this.disable_list_update = true;
						bulk_operations.delete(docnames, () => {
							this.disable_list_update = false;
							this.clear_checked_items();
							this.refresh();
						});
					});
				},
				standard: true,
			};
		};

		const bulk_cancel = () => {
			return {
				label: __("Cancel", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);
					if (docnames.length > 0) {
						frappe.confirm(
							__(
								"Cancel {0} documents?",
								[docnames.length],
								"Title of confirmation dialog"
							),
							() => {
								this.disable_list_update = true;
								bulk_operations.submit_or_cancel(docnames, "cancel", () => {
									this.disable_list_update = false;
									this.clear_checked_items();
									this.refresh();
								});
							}
						);
					}
				},
				standard: true,
			};
		};

		const bulk_submit = () => {
			return {
				label: __("Submit", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);
					if (docnames.length > 0) {
						frappe.confirm(
							__(
								"Submit {0} documents?",
								[docnames.length],
								"Title of confirmation dialog"
							),
							() => {
								this.disable_list_update = true;
								bulk_operations.submit_or_cancel(docnames, "submit", () => {
									this.disable_list_update = false;
									this.clear_checked_items();
									this.refresh();
								});
							}
						);
					}
				},
				standard: true,
			};
		};

		const bulk_edit = () => {
			return {
				label: __("Edit", null, "Button in list view actions menu"),
				action: () => {
					let field_mappings = {};

					frappe.meta.get_docfields(doctype).forEach((field_doc) => {
						if (is_field_editable(field_doc)) {
							field_mappings[field_doc.label] = Object.assign({}, field_doc);
						}
					});

					this.disable_list_update = true;
					bulk_operations.edit(this.get_checked_items(true), field_mappings, () => {
						this.disable_list_update = false;
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_export = () => {
			return {
				label: __("Export", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);

					bulk_operations.export(doctype, docnames);
				},
				standard: true,
			};
		};

		// bulk edit
		if (has_editable_fields(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_edit());
		}

		actions_menu_items.push(bulk_export());

		// bulk assignment
		actions_menu_items.push(bulk_assignment());

		actions_menu_items.push(bulk_assignment_rule());

		actions_menu_items.push(bulk_add_tags());

		// bulk printing
		if (frappe.model.can_print(doctype)) {
			actions_menu_items.push(bulk_printing());
		}

		// bulk submit
		if (
			frappe.model.is_submittable(doctype) &&
			has_submit_permission(doctype) &&
			!frappe.model.has_workflow(doctype)
		) {
			actions_menu_items.push(bulk_submit());
		}

		// bulk cancel
		if (frappe.model.can_cancel(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_cancel());
		}

		// bulk delete
		if (frappe.model.can_delete(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_delete());
		}

		return actions_menu_items;
	}

	parse_filters_from_route_options() {
		const filters = [];

		for (let field in frappe.route_options) {
			let doctype = null;
			let value = frappe.route_options[field];

			let value_array;
			if ($.isArray(value) && value[0].startsWith("[") && value[0].endsWith("]")) {
				value_array = [];
				for (var i = 0; i < value.length; i++) {
					value_array.push(JSON.parse(value[i]));
				}
			} else if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
				value = JSON.parse(value);
			}

			// if `Child DocType.fieldname`
			if (field.includes(".")) {
				doctype = field.split(".")[0];
				field = field.split(".")[1];
			}

			// find the table in which the key exists
			// for example the filter could be {"item_code": "X"}
			// where item_code is in the child table.

			// we can search all tables for mapping the doctype
			if (!doctype) {
				doctype = frappe.meta.get_doctype_for_field(this.doctype, field);
			}

			if (doctype) {
				if (value_array) {
					for (var j = 0; j < value_array.length; j++) {
						if ($.isArray(value_array[j])) {
							filters.push([doctype, field, value_array[j][0], value_array[j][1]]);
						} else {
							filters.push([doctype, field, "=", value_array[j]]);
						}
					}
				} else if ($.isArray(value)) {
					filters.push([doctype, field, value[0], value[1]]);
				} else {
					filters.push([doctype, field, "=", value]);
				}
			}
		}

		return filters;
	}
};

frappe.get_list_view = (doctype) => {
	let route = `List/${doctype}/List`;
	return frappe.views.list_view[route];
};

class ElementFactory {
	/* Pre-create templates for HTML Elements on initialization and provide them
	via the get_xxx_element methods. */
	constructor(doctype) {
		this.templates = {
			checkbox: this.create_checkbox_element(doctype),
			checkboxspan: this.create_checkboxspan_element(),
			link: this.create_link_element(doctype),
			like: this.create_like_element(doctype),
		};
	}

	create_checkbox_element(doctype) {
		const checkbox = document.createElement("input");
		checkbox.classList.add("list-row-checkbox");
		checkbox.type = "checkbox";
		checkbox.dataset.doctype = doctype;
		return checkbox;
	}

	create_link_element(doctype) {
		const link = document.createElement("a");
		link.classList.add("ellipsis");
		link.dataset.doctype = doctype;

		return link;
	}

	create_checkboxspan_element() {
		const checkboxspan = document.createElement("span");
		checkboxspan.classList.add("level-item", "select-like");

		return checkboxspan;
	}

	create_like_element(doctype) {
		const like = document.createElement("span");
		like.classList.add("like-action");
		like.innerHTML = frappe.utils.icon("es-solid-heart", "sm", "like-icon");
		like.dataset.doctype = doctype;

		return like;
	}

	get_checkbox_element(name) {
		const checkbox = this.templates.checkbox.cloneNode(true);
		checkbox.dataset.name = name;
		return checkbox;
	}

	get_checkboxspan_element() {
		return this.templates.checkboxspan.cloneNode(true);
	}

	get_link_element(name, href, text) {
		const link = this.templates.link.cloneNode(true);
		link.dataset.name = name;
		link.href = href;
		link.title = text;
		link.textContent = text;

		return link;
	}

	get_like_element(name, liked, liked_by, title) {
		const like = this.templates.like.cloneNode(true);
		like.dataset.name = name;

		const heart_classes = liked ? ["liked-by", "liked"] : ["not-liked"];
		like.classList.add(...heart_classes);

		like.setAttribute("data-liked-by", liked_by || "[]");
		like.setAttribute("title", title);

		return like;
	}
}

./list_view_select.js

frappe.provide("frappe.views");

frappe.views.ListViewSelect = class ListViewSelect {
	constructor(opts) {
		$.extend(this, opts);
		this.set_current_view();
		this.setup_views();
	}

	add_view_to_menu(view, action) {
		if (this.doctype == "File" && view == "List") {
			view = "File";
		}
		let $el = this.page.add_custom_menu_item(
			this.parent,
			__(view),
			action,
			true,
			null,
			this.icon_map[view] || "list"
		);
		$el.parent().attr("data-view", view);
	}

	set_current_view() {
		this.current_view = "List";
		const route = frappe.get_route();
		const view_name = frappe.utils.to_title_case(route[2] || "");
		if (route.length > 2 && frappe.views.view_modes.includes(view_name)) {
			this.current_view = view_name;

			if (this.current_view === "Kanban") {
				this.kanban_board = route[3];
			} else if (this.current_view === "Inbox") {
				this.email_account = route[3];
			}
		}
	}

	set_route(view, calendar_name) {
		const route = [this.slug(), "view", view];
		if (calendar_name) route.push(calendar_name);

		let search_params = cur_list?.get_search_params();
		if (search_params) {
			frappe.route_options = Object.fromEntries(search_params);
		}
		frappe.set_route(route);
	}

	setup_views() {
		const views = {
			List: {
				condition: true,
				action: () => this.set_route("list"),
			},
			Report: {
				condition: true,
				action: () => this.set_route("report"),
				current_view_handler: () => {
					const reports = this.get_reports();
					let default_action = {};
					// Only add action if current route is not report builder
					if (frappe.get_route().length > 3) {
						default_action = {
							label: __("Report Builder"),
							action: () => this.set_route("report"),
						};
					}
					this.setup_dropdown_in_sidebar("Report", reports, default_action);
				},
			},
			Dashboard: {
				condition: true,
				action: () => this.set_route("dashboard"),
			},
			Calendar: {
				condition: frappe.views.calendar[this.doctype],
				action: () => this.set_route("calendar", "default"),
				current_view_handler: () => {
					this.get_calendars().then((calendars) => {
						this.setup_dropdown_in_sidebar("Calendar", calendars);
					});
				},
			},
			Gantt: {
				condition: frappe.views.calendar[this.doctype],
				action: () => this.set_route("gantt"),
			},
			Inbox: {
				condition: this.doctype === "Communication" && frappe.boot.email_accounts.length,
				action: () => this.set_route("inbox"),
				current_view_handler: () => {
					const accounts = this.get_email_accounts();
					let default_action;
					if (has_common(frappe.user_roles, ["System Manager", "Administrator"])) {
						default_action = {
							label: __("New Email Account"),
							action: () => frappe.new_doc("Email Account"),
						};
					}
					this.setup_dropdown_in_sidebar("Inbox", accounts, default_action);
				},
			},
			Image: {
				condition: this.list_view.meta.image_field,
				action: () => this.set_route("image"),
			},
			Tree: {
				condition:
					frappe.treeview_settings[this.doctype] ||
					frappe.get_meta(this.doctype).is_tree,
				action: () => this.set_route("tree"),
			},
			Kanban: {
				condition: this.doctype != "File",
				action: () => this.setup_kanban_boards(),
				current_view_handler: () => {
					frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) =>
						this.setup_kanban_switcher(kanbans)
					);
				},
			},
			Map: {
				condition:
					this.list_view.settings.get_coords_method ||
					(this.list_view.meta.fields.find((i) => i.fieldname === "latitude") &&
						this.list_view.meta.fields.find((i) => i.fieldname === "longitude")) ||
					this.list_view.meta.fields.find(
						(i) => i.fieldname === "location" && i.fieldtype == "Geolocation"
					),
				action: () => this.set_route("map"),
			},
		};

		frappe.views.view_modes.forEach((view) => {
			if (this.current_view !== view && views[view].condition) {
				this.add_view_to_menu(view, views[view].action);
			}

			if (this.current_view == view) {
				views[view].current_view_handler && views[view].current_view_handler();
			}
		});
	}

	setup_dropdown_in_sidebar(view, items, default_action) {
		if (!this.sidebar) return;
		const views_wrapper = this.sidebar.sidebar.find(".views-section");
		views_wrapper.find(".sidebar-label").html(__(view));
		const $dropdown = views_wrapper.find(".views-dropdown");

		let placeholder = __("Select {0}", [__(view)]);
		let html = ``;

		if (!items || !items.length) {
			html = `<div class="empty-state">
						${__("No {0} Found", [__(view)])}
				</div>`;
		} else {
			const page_name = this.get_page_name();
			items.map((item) => {
				if (item.name.toLowerCase() == page_name.toLowerCase()) {
					placeholder = item.name;
				} else {
					html += `<li><a class="dropdown-item" href="${item.route}">${item.name}</a></li>`;
				}
			});
		}

		views_wrapper.find(".selected-view").html(placeholder);

		if (default_action) {
			views_wrapper.find(".sidebar-action a").html(default_action.label);
			views_wrapper.find(".sidebar-action a").click(() => default_action.action());
		}

		$dropdown.html(html);

		views_wrapper.removeClass("hide");
	}

	setup_kanban_switcher(kanbans) {
		const kanban_switcher = this.page.add_custom_button_group(
			__("Select Kanban"),
			null,
			this.list_view.$filter_section
		);

		kanbans.map((k) => {
			this.page.add_custom_menu_item(
				kanban_switcher,
				k.name,
				() => this.set_route("kanban", k.name),
				false
			);
		});

		let perms = this.list_view.board_perms;
		let can_create = perms ? perms.create : true;
		if (can_create) {
			this.page.add_custom_menu_item(
				kanban_switcher,
				__("Create New Kanban Board"),
				() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
				true
			);
		}
	}

	get_page_name() {
		return frappe.utils.to_title_case(frappe.get_route().slice(-1)[0] || "");
	}

	get_reports() {
		// add reports linked to this doctype to the dropdown
		let added = [];
		let reports_to_add = [];

		let add_reports = (reports) => {
			reports.map((r) => {
				if (!r.ref_doctype || r.ref_doctype == this.doctype) {
					const report_type =
						r.report_type === "Report Builder"
							? `/app/list/${r.ref_doctype}/report`
							: "/app/query-report";

					const route = r.route || report_type + "/" + (r.title || r.name);

					if (added.indexOf(route) === -1) {
						// don't repeat
						added.push(route);
						reports_to_add.push({
							name: __(r.title || r.name),
							route: route,
						});
					}
				}
			});
		};

		// from reference doctype
		if (this.list_view.settings.reports) {
			add_reports(this.list_view.settings.reports);
		}

		// Sort reports alphabetically
		var reports =
			Object.values(frappe.boot.user.all_reports).sort((a, b) =>
				a.title.localeCompare(b.title)
			) || [];

		// from specially tagged reports
		add_reports(reports);

		return reports_to_add;
	}

	setup_kanban_boards() {
		function fetch_kanban_board(doctype) {
			frappe.db.get_value(
				"Kanban Board",
				{ reference_doctype: doctype },
				"name",
				(board) => {
					if (!$.isEmptyObject(board)) {
						frappe.set_route("list", doctype, "kanban", board.name);
					} else {
						frappe.views.KanbanView.show_kanban_dialog(doctype);
					}
				}
			);
		}

		const last_opened_kanban =
			frappe.model.user_settings[this.doctype]["Kanban"]?.last_kanban_board;
		if (!last_opened_kanban) {
			fetch_kanban_board(this.doctype);
		} else {
			frappe.db.exists("Kanban Board", last_opened_kanban).then((exists) => {
				if (exists) {
					frappe.set_route("list", this.doctype, "kanban", last_opened_kanban);
				} else {
					fetch_kanban_board(this.doctype);
				}
			});
		}
	}

	get_calendars() {
		const doctype = this.doctype;
		let calendars = [];

		return frappe.db
			.get_list("Calendar View", {
				filters: {
					reference_doctype: doctype,
				},
			})
			.then((result) => {
				if (!(result && Array.isArray(result) && result.length)) return;

				if (frappe.views.calendar[this.doctype]) {
					// has standard calendar view
					calendars.push({
						name: "Default",
						route: `/app/${this.slug()}/view/calendar/default`,
					});
				}
				result.map((calendar) => {
					calendars.push({
						name: calendar.name,
						route: `/app/${this.slug()}/view/calendar/${calendar.name}`,
					});
				});

				return calendars;
			});
	}

	get_email_accounts() {
		let accounts_to_add = [];
		let accounts = frappe.boot.email_accounts;
		accounts.forEach((account) => {
			let email_account =
				account.email_id == "All Accounts" ? "All Accounts" : account.email_account;
			let route = `/app/communication/view/inbox/${email_account}`;
			let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(
				account.email_id
			)
				? __(account.email_id)
				: account.email_account;

			accounts_to_add.push({
				name: display_name,
				route: route,
			});
		});

		return accounts_to_add;
	}

	slug() {
		return frappe.router.slug(frappe.router.doctype_layout || this.doctype);
	}
};

./list_filter.js

frappe.provide("frappe.ui");

export default class ListFilter {
	constructor({ wrapper, doctype }) {
		Object.assign(this, arguments[0]);
		this.can_add_global = frappe.user.has_role(["System Manager", "Administrator"]);
		this.filters = [];
		this.make();
		this.bind();
		this.refresh();
	}

	make() {
		// init dom
		this.wrapper.html(`
			<li class="input-area"></li>
			<li class="sidebar-action">
				<a class="saved-filters-preview">${__("Show Saved")}</a>
			</li>
			<div class="saved-filters"></div>
		`);

		this.$input_area = this.wrapper.find(".input-area");
		this.$list_filters = this.wrapper.find(".list-filters");
		this.$saved_filters = this.wrapper.find(".saved-filters").hide();
		this.$saved_filters_preview = this.wrapper.find(".saved-filters-preview");
		this.saved_filters_hidden = true;
		this.toggle_saved_filters(true);

		this.filter_input = frappe.ui.form.make_control({
			df: {
				fieldtype: "Data",
				placeholder: __("Filter Name"),
				input_class: "input-xs",
			},
			parent: this.$input_area,
			render_input: 1,
		});

		this.is_global_input = frappe.ui.form.make_control({
			df: {
				fieldtype: "Check",
				label: __("Is Global"),
			},
			parent: this.$input_area,
			render_input: 1,
		});
	}

	bind() {
		this.bind_save_filter();
		this.bind_toggle_saved_filters();
		this.bind_click_filter();
		this.bind_remove_filter();
	}

	refresh() {
		this.get_list_filters().then(() => {
			this.filters.length
				? this.$saved_filters_preview.show()
				: this.$saved_filters_preview.hide();
			const html = this.filters.map((filter) => this.filter_template(filter));
			this.wrapper.find(".filter-pill").remove();
			this.$saved_filters.append(html);
		});
		this.is_global_input.toggle(false);
		this.filter_input.set_description("");
	}

	filter_template(filter) {
		return `<div class="list-link filter-pill list-sidebar-button btn btn-default" data-name="${
			filter.name
		}">
			<a class="ellipsis filter-name">${filter.filter_name}</a>
			<a class="remove">${frappe.utils.icon("close")}</a>
		</div>`;
	}

	bind_toggle_saved_filters() {
		this.wrapper.find(".saved-filters-preview").click(() => {
			this.toggle_saved_filters(this.saved_filters_hidden);
		});
	}

	toggle_saved_filters(show) {
		this.$saved_filters.toggle(show);
		const label = show ? __("Hide Saved") : __("Show Saved");
		this.wrapper.find(".saved-filters-preview").text(label);
		this.saved_filters_hidden = !this.saved_filters_hidden;
	}

	bind_click_filter() {
		this.wrapper.on("click", ".filter-pill .filter-name", (e) => {
			let $filter = $(e.currentTarget).parent(".filter-pill");
			this.set_applied_filter($filter);
			const name = $filter.attr("data-name");
			this.list_view.filter_area.clear().then(() => {
				this.list_view.filter_area.add(this.get_filters_values(name));
			});
		});
	}

	bind_remove_filter() {
		this.wrapper.on("click", ".filter-pill .remove", (e) => {
			const $li = $(e.currentTarget).closest(".filter-pill");
			const filter_label = $li.text().trim();

			frappe.confirm(
				__("Are you sure you want to remove the {0} filter?", [filter_label.bold()]),
				() => {
					const name = $li.attr("data-name");
					const applied_filters = this.get_filters_values(name);
					$li.remove();
					this.remove_filter(name).then(() => this.refresh());
					this.list_view.filter_area.remove_filters(applied_filters);
				}
			);
		});
	}

	bind_save_filter() {
		this.filter_input.$input.keydown(
			frappe.utils.debounce((e) => {
				const value = this.filter_input.get_value();
				const has_value = Boolean(value);

				if (e.which === frappe.ui.keyCode["ENTER"]) {
					if (!has_value || this.filter_name_exists(value)) return;

					this.filter_input.set_value("");
					this.save_filter(value).then(() => this.refresh());
					this.toggle_saved_filters(true);
				} else {
					let help_text = __("Press Enter to save");

					if (this.filter_name_exists(value)) {
						help_text = __("Duplicate Filter Name");
					}

					this.filter_input.set_description(has_value ? help_text : "");

					if (this.can_add_global) {
						this.is_global_input.toggle(has_value);
					}
				}
			}, 300)
		);
	}

	save_filter(filter_name) {
		return frappe.db.insert({
			doctype: "List Filter",
			reference_doctype: this.list_view.doctype,
			filter_name,
			for_user: this.is_global_input.get_value() ? "" : frappe.session.user,
			filters: JSON.stringify(this.get_current_filters()),
		});
	}

	remove_filter(name) {
		if (!name) return;
		return frappe.db.delete_doc("List Filter", name);
	}

	get_filters_values(name) {
		const filter = this.filters.find((filter) => filter.name === name);
		return JSON.parse(filter.filters || "[]");
	}

	get_current_filters() {
		return this.list_view.filter_area.get();
	}

	filter_name_exists(filter_name) {
		return (this.filters || []).find((f) => f.filter_name === filter_name);
	}

	get_list_filters() {
		if (frappe.session.user === "Guest") return Promise.resolve();
		return frappe.db
			.get_list("List Filter", {
				fields: ["name", "filter_name", "for_user", "filters"],
				filters: { reference_doctype: this.list_view.doctype },
				or_filters: [
					["for_user", "=", frappe.session.user],
					["for_user", "=", ""],
				],
			})
			.then((filters) => {
				this.filters = filters || [];
			});
	}

	set_applied_filter($filter) {
		this.$saved_filters
			.find(".btn-primary-light")
			.toggleClass("btn-primary-light btn-default");
		$filter.toggleClass("btn-default btn-primary-light");
	}
}

./list_factory.js

// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.provide("frappe.views.list_view");

window.cur_list = null;
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
	make(route) {
		const me = this;
		const doctype = route[1];

		// List / Gantt / Kanban / etc
		let view_name = frappe.utils.to_title_case(route[2] || "List");

		// File is a special view
		if (doctype == "File" && !["Report", "Dashboard"].includes(view_name)) {
			view_name = "File";
		}

		let view_class = frappe.views[view_name + "View"];
		if (!view_class) view_class = frappe.views.ListView;

		if (view_class && view_class.load_last_view && view_class.load_last_view()) {
			// view can have custom routing logic
			return;
		}

		frappe.provide("frappe.views.list_view." + doctype);

		frappe.views.list_view[me.page_name] = new view_class({
			doctype: doctype,
			parent: me.make_page(true, me.page_name),
		});

		me.set_cur_list();
	}

	before_show() {
		if (this.re_route_to_view()) {
			return false;
		}

		this.set_module_breadcrumb();
	}

	on_show() {
		this.set_cur_list();
		if (cur_list) cur_list.show();
	}

	re_route_to_view() {
		const doctype = this.route[1];
		const last_route = frappe.route_history.slice(-2)[0];
		if (
			this.route[0] === "List" &&
			this.route.length === 2 &&
			frappe.views.list_view[doctype] &&
			last_route &&
			last_route[0] === "List" &&
			last_route[1] === doctype
		) {
			// last route same as this route, so going back.
			// this happens because /app/List/Item will redirect to /app/List/Item/List
			// while coming from back button, the last 2 routes will be same, so
			// we know user is coming in the reverse direction (via back button)

			// example:
			// Step 1: /app/List/Item redirects to /app/List/Item/List
			// Step 2: User hits "back" comes back to /app/List/Item
			// Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
			window.history.go(-1);
			return true;
		}
	}

	set_module_breadcrumb() {
		if (frappe.route_history.length > 1) {
			const prev_route = frappe.route_history[frappe.route_history.length - 2];
			if (prev_route[0] === "modules") {
				const doctype = this.route[1],
					module = prev_route[1];
				if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) {
					// save the last page from the breadcrumb was accessed
					frappe.breadcrumbs.set_doctype_module(doctype, module);
				}
			}
		}
	}

	set_cur_list() {
		cur_list = frappe.views.list_view[this.page_name];
		if (cur_list && cur_list.doctype !== this.route[1]) {
			// changing...
			window.cur_list = null;
		}
	}
};

Frappe Framework is a python framework on which ERPNext is build. Below is the code of various files, with which the Kanban Implementation in Frappe Framework is done.

Analyse & understand the codes given below, try if you can come up with a step by step plan on how we can add a new view as "Kanban" by the name "DeepSight" to the frappe.views. And then make it accessible via the select list? Don't output full code, just share relevant code that needs to be added in various files. Changes that need to be done with short reasoning.

The code: basepath: frappe/frappe/public/js/frappe/views/kanban/ ./kanban_view.js

import KanbanSettings from "./kanban_settings";

frappe.provide("frappe.views");

frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
	static load_last_view() {
		const route = frappe.get_route();
		if (route.length === 3) {
			const doctype = route[1];
			const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {};
			if (!user_settings.last_kanban_board) {
				return new frappe.views.KanbanView({ doctype: doctype });
			}

			route.push(user_settings.last_kanban_board);
			frappe.set_route(route);
			return true;
		}
		return false;
	}

	get view_name() {
		return "Kanban";
	}

	show() {
		frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) => {
			if (!kanbans.length) {
				return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
			} else if (kanbans.length && frappe.get_route().length !== 4) {
				return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
			} else {
				this.kanbans = kanbans;

				return frappe.run_serially([
					() => this.show_skeleton(),
					() => this.fetch_meta(),
					() => this.hide_skeleton(),
					() => this.check_permissions(),
					() => this.init(),
					() => this.before_refresh(),
					() => this.refresh(),
				]);
			}
		});
	}

	init() {
		return super.init().then(() => {
			let menu_length = this.page.menu.find(".dropdown-item").length;
			if (menu_length === 1) {
				// Only 'Refresh' (hidden) is present (always), dropdown is visibly empty
				this.page.hide_menu();
			}
		});
	}

	setup_defaults() {
		return super.setup_defaults().then(() => {
			let get_board_name = () => {
				return this.kanbans.length && this.kanbans[0].name;
			};

			this.board_name = frappe.get_route()[3] || get_board_name() || null;
			this.page_title = __(this.board_name);
			this.card_meta = this.get_card_meta();
			this.page_length = 0;

			return frappe.run_serially([
				() => this.set_board_perms_and_push_menu_items(),
				() => this.get_board(),
			]);
		});
	}

	set_board_perms_and_push_menu_items() {
		// needs server-side call as client-side document instance is absent before kanban render
		return frappe.call({
			method: "frappe.client.get_doc_permissions",
			args: {
				doctype: "Kanban Board",
				docname: this.board_name,
			},
			callback: (result) => {
				this.board_perms = result.message.permissions || {};
				this.push_menu_items();
			},
		});
	}

	push_menu_items() {
		if (this.board_perms.write) {
			this.menu_items.push({
				label: __("Save filters"),
				action: () => {
					this.save_kanban_board_filters();
				},
			});
		}

		if (this.board_perms.delete) {
			this.menu_items.push({
				label: __("Delete Kanban Board"),
				action: () => {
					frappe.confirm(__("Are you sure you want to proceed?"), () => {
						frappe.db.delete_doc("Kanban Board", this.board_name).then(() => {
							frappe.show_alert(`Kanban Board ${this.board_name} deleted.`);
							frappe.set_route("List", this.doctype, "List");
						});
					});
				},
			});
		}
	}

	setup_paging_area() {
		// pass
	}

	toggle_result_area() {
		this.$result.toggle(this.data.length > 0);
	}

	get_board() {
		return frappe.db.get_doc("Kanban Board", this.board_name).then((board) => {
			this.board = board;
			this.board.filters_array = JSON.parse(this.board.filters || "[]");
			this.board.fields = JSON.parse(this.board.fields || "[]");
			this.filters = this.board.filters_array;
		});
	}

	setup_page() {
		this.hide_sidebar = true;
		this.hide_page_form = true;
		this.hide_card_layout = true;
		this.hide_sort_selector = true;
		super.setup_page();
	}

	setup_view() {
		if (this.board.columns.length > 5) {
			this.page.container.addClass("full-width");
		}
		this.setup_realtime_updates();
		this.setup_like();
	}

	set_fields() {
		super.set_fields();
		this._add_field(this.card_meta.title_field);
	}

	before_render() {
		frappe.model.user_settings.save(this.doctype, "last_view", this.view_name);
		this.save_view_user_settings({
			last_kanban_board: this.board_name,
		});
	}

	render_list() {}

	on_filter_change() {
		if (!this.board_perms.write) return; // avoid misleading ux

		if (JSON.stringify(this.board.filters_array) !== JSON.stringify(this.filter_area.get())) {
			this.page.set_indicator(__("Not Saved"), "orange");
		} else {
			this.page.clear_indicator();
		}
	}

	save_kanban_board_filters() {
		const filters = this.filter_area.get();

		frappe.db.set_value("Kanban Board", this.board_name, "filters", filters).then((r) => {
			if (r.exc) {
				frappe.show_alert({
					indicator: "red",
					message: __("There was an error saving filters"),
				});
				return;
			}
			frappe.show_alert({
				indicator: "green",
				message: __("Filters saved"),
			});

			this.board.filters_array = filters;
			this.on_filter_change();
		});
	}

	get_fields() {
		this.fields.push([this.board.field_name, this.board.reference_doctype]);
		return super.get_fields();
	}

	render() {
		const board_name = this.board_name;
		if (!this.kanban) {
			this.kanban = new frappe.views.KanbanBoard({
				doctype: this.doctype,
				board: this.board,
				board_name: board_name,
				cards: this.data,
				card_meta: this.card_meta,
				wrapper: this.$result,
				cur_list: this,
				user_settings: this.view_user_settings,
			});
		} else if (board_name === this.kanban.board_name) {
			this.kanban.update(this.data);
		}
	}

	get_card_meta() {
		var meta = frappe.get_meta(this.doctype);
		// preserve route options erased by new doc
		let route_options = { ...frappe.route_options };
		var doc = frappe.model.get_new_doc(this.doctype);
		frappe.route_options = route_options;
		var title_field = null;
		var quick_entry = false;

		if (this.meta.title_field) {
			title_field = frappe.meta.get_field(this.doctype, this.meta.title_field);
		}

		this.meta.fields.forEach((df) => {
			const is_valid_field =
				["Data", "Text", "Small Text", "Text Editor"].includes(df.fieldtype) && !df.hidden;

			if (is_valid_field && !title_field) {
				// can be mapped to textarea
				title_field = df;
			}
		});

		// quick entry
		var mandatory = meta.fields.filter((df) => df.reqd && !doc[df.fieldname]);

		if (
			mandatory.some((df) => frappe.model.table_fields.includes(df.fieldtype)) ||
			mandatory.length > 1
		) {
			quick_entry = true;
		}

		if (!title_field) {
			title_field = frappe.meta.get_field(this.doctype, "name");
		}

		return {
			quick_entry: quick_entry,
			title_field: title_field,
		};
	}

	get_view_settings() {
		return {
			label: __("Kanban Settings", null, "Button in kanban view menu"),
			action: () => this.show_kanban_settings(),
			standard: true,
		};
	}

	show_kanban_settings() {
		frappe.model.with_doctype(this.doctype, () => {
			new KanbanSettings({
				kanbanview: this,
				doctype: this.doctype,
				settings: this.board,
				meta: frappe.get_meta(this.doctype),
			});
		});
	}

	get required_libs() {
		return "kanban_board.bundle.js";
	}
};

frappe.views.KanbanView.get_kanbans = function (doctype) {
	let kanbans = [];

	return get_kanban_boards().then((kanban_boards) => {
		if (kanban_boards) {
			kanban_boards.forEach((board) => {
				let route = `/app/${frappe.router.slug(board.reference_doctype)}/view/kanban/${
					board.name
				}`;
				kanbans.push({ name: board.name, route: route });
			});
		}

		return kanbans;
	});

	function get_kanban_boards() {
		return frappe
			.call("frappe.desk.doctype.kanban_board.kanban_board.get_kanban_boards", { doctype })
			.then((r) => r.message);
	}
};

frappe.views.KanbanView.show_kanban_dialog = function (doctype) {
	let dialog = new_kanban_dialog();
	dialog.show();

	function make_kanban_board(board_name, field_name, project) {
		return frappe.call({
			method: "frappe.desk.doctype.kanban_board.kanban_board.quick_kanban_board",
			args: {
				doctype,
				board_name,
				field_name,
				project,
			},
			callback: function (r) {
				var kb = r.message;
				if (kb.filters) {
					frappe.provide("frappe.kanban_filters");
					frappe.kanban_filters[kb.kanban_board_name] = kb.filters;
				}
				frappe.set_route("List", doctype, "Kanban", kb.kanban_board_name);
			},
		});
	}

	function new_kanban_dialog() {
		/* Kanban dialog can show either "Save" or "Customize Form" option depending if any Select fields exist in the DocType for Kanban creation
		 */

		const select_fields = frappe.get_meta(doctype).fields.filter((df) => {
			return df.fieldtype === "Select" && df.fieldname !== "kanban_column";
		});
		const dialog_fields = get_fields_for_dialog(select_fields);
		const to_save = select_fields.length > 0;
		const primary_action_label = to_save ? __("Save") : __("Customize Form");
		const dialog_title = to_save ? __("New Kanban Board") : __("No Select Field Found");

		let primary_action = () => {
			if (to_save) {
				const values = dialog.get_values();
				make_kanban_board(values.board_name, values.field_name, values.project).then(
					() => dialog.hide(),
					(err) => frappe.msgprint(err)
				);
			} else {
				frappe.set_route("Form", "Customize Form", { doc_type: doctype });
			}
		};

		return new frappe.ui.Dialog({
			title: dialog_title,
			fields: dialog_fields,
			primary_action_label,
			primary_action,
		});
	}

	function get_fields_for_dialog(select_fields) {
		if (!select_fields.length) {
			return [
				{
					fieldtype: "HTML",
					options: `
					<div>
						<p class="text-medium">
						${__(
							'No fields found that can be used as a Kanban Column. Use the Customize Form to add a Custom Field of type "Select".'
						)}
						</p>
					</div>
				`,
				},
			];
		}

		let fields = [
			{
				fieldtype: "Data",
				fieldname: "board_name",
				label: __("Kanban Board Name"),
				reqd: 1,
				description: ["Note", "ToDo"].includes(doctype)
					? __("This Kanban Board will be private")
					: "",
			},
			{
				fieldtype: "Select",
				fieldname: "field_name",
				label: __("Columns based on"),
				options: select_fields.map((df) => ({ label: df.label, value: df.fieldname })),
				default: select_fields[0],
				reqd: 1,
			},
		];

		if (doctype === "Task") {
			fields.push({
				fieldtype: "Link",
				fieldname: "project",
				label: __("Project"),
				options: "Project",
			});
		}

		return fields;
	}
};


./kanban_column.html
html
<div class="kanban-column" data-column-value="{{title}}">
	<div class="kanban-column-header">
		<span class="kanban-column-title">
			<span class="indicator-pill {{indicator}}"></span>
			<span class="kanban-title ellipsis" title="{{title}}">{{ __(title) }}</span>
		</span>
		<div class="column-options dropdown pull-right">
			<a data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
				<svg class="icon icon-sm">
					<use href="#icon-dot-horizontal"></use>
				</svg>
			</a>
			<ul class="dropdown-menu" style="max-height: 300px; overflow-y: auto;">
				<li><a class="dropdown-item" data-action="archive">{{ __("Archive") }}</a></li>
			</ul>
		</div>
	</div>
	<div class="add-card">
		<div class="ellipsis">
			+ {{ __("Add {0}", [__(doctype)]) }}
		</div>
	</div>
	<div class="kanban-card new-card-area">
		<textarea name="title"></textarea>
	</div>
	<div class="kanban-cards">
	</div>
</div>


./kanban_card.html
html
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
	<div class="kanban-card content">
		{% if(image_url) { %}
		<div class="kanban-image">
			<img  src="{{image_url}}" alt="{{title}}">
		</div>
		{% } %}
		<div class="kanban-card-body">
			<div class="kanban-title-area">
				<a href="{{ form_link }}">
					<div class="kanban-card-title ellipsis" title="{{title}}">
						{{ title }}
					</div>
				</a>
				<br>
				<div class="kanban-card-doc text-muted">
					{{ doc_content }}
				</div>
			</div>
			<div class="kanban-card-meta">
			</div>
		</div>
	</div>
</div>



./kanban_board.html
html
<div class="kanban">
	<div class="kanban-column add-new-column">
		<div class="kanban-column-title compose-column">
			<a> + {{ __("Add Column") }}</a>
		</div>
		<form class="compose-column-form kanban-column-title">
			<input class="new-column-title" name="title" type="text" autocomplete="off">
		</form>
	</div>
	<div class="kanban-empty-state text-muted text-center" style="display: none;">
		{{ __("Loading...") }}
	</div>
</div>

./kanban_board.bundle.js

// TODO: Refactor for better UX

import { createStore } from "vuex";

frappe.provide("frappe.views");

(function () {
	var method_prefix = "frappe.desk.doctype.kanban_board.kanban_board.";

	let columns_unwatcher = null;

	var store = createStore({
		state: {
			doctype: "",
			board: {},
			card_meta: {},
			cards: [],
			columns: [],
			filters_modified: false,
			cur_list: {},
			empty_state: true,
		},
		mutations: {
			update_state(state, obj) {
				Object.assign(state, obj);
			},
		},
		actions: {
			init: function (context, opts) {
				context.commit("update_state", {
					empty_state: true,
				});
				var board = opts.board;
				var card_meta = opts.card_meta;
				opts.card_meta = card_meta;
				opts.board = board;
				var cards = opts.cards.map(function (card) {
					return prepare_card(card, opts);
				});
				var columns = prepare_columns(board.columns);
				context.commit("update_state", {
					doctype: opts.doctype,
					board: board,
					card_meta: card_meta,
					cards: cards,
					columns: columns,
					cur_list: opts.cur_list,
					empty_state: false,
					wrapper: opts.wrapper,
				});
			},
			update_cards: function (context, cards) {
				var state = context.state;
				var _cards = cards
					.map((card) => prepare_card(card, state))
					.concat(state.cards)
					.uniqBy((card) => card.name);

				context.commit("update_state", {
					cards: _cards,
				});
			},
			add_column: function (context, col) {
				if (frappe.model.can_create("Custom Field")) {
					store.dispatch("update_column", { col, action: "add" });
				} else {
					frappe.msgprint({
						title: __("Not permitted"),
						message: __("You are not allowed to create columns"),
						indicator: "red",
					});
				}
			},
			archive_column: function (context, col) {
				store.dispatch("update_column", { col, action: "archive" });
			},
			restore_column: function (context, col) {
				store.dispatch("update_column", { col, action: "restore" });
			},
			update_column: function (context, { col, action }) {
				var doctype = context.state.doctype;
				var board = context.state.board;
				fetch_customization(doctype)
					.then(function (doc) {
						return modify_column_field_in_c11n(doc, board, col.title, action);
					})
					.then(save_customization)
					.then(function () {
						return update_kanban_board(board.name, col.title, action);
					})
					.then(
						function (r) {
							var cols = r.message;
							context.commit("update_state", {
								columns: prepare_columns(cols),
							});
						},
						function (err) {
							console.error(err);
						}
					);
			},
			add_card: function (context, { card_title, column_title }) {
				var state = context.state;
				var doc = frappe.model.get_new_doc(state.doctype);
				var field = state.card_meta.title_field;
				var quick_entry = state.card_meta.quick_entry;

				var doc_fields = {};
				doc_fields[field.fieldname] = card_title;
				doc_fields[state.board.field_name] = column_title;
				state.cur_list.filter_area.get().forEach(function (f) {
					if (f[2] !== "=") return;
					doc_fields[f[1]] = f[3];
				});

				$.extend(doc, doc_fields);

				// add the card directly
				// for better ux
				const card = prepare_card(doc, state);
				card._disable_click = true;
				const cards = [...state.cards, card];
				// remember the name which we will override later
				const old_name = doc.name;
				context.commit("update_state", { cards });

				if (field && !quick_entry) {
					return insert_doc(doc).then(function (r) {
						// update the card in place with the updated doc
						const updated_doc = r.message;
						const index = state.cards.findIndex((card) => card.name === old_name);
						const card = prepare_card(updated_doc, state);
						const new_cards = state.cards.slice();
						new_cards[index] = card;
						context.commit("update_state", { cards: new_cards });
						const args = {
							new: 1,
							name: card.name,
							colname: updated_doc[state.board.field_name],
						};
						store.dispatch("update_order_for_single_card", args);
					});
				} else {
					frappe.new_doc(state.doctype, doc);
				}
			},
			update_card: function (context, card) {
				var index = -1;
				context.state.cards.forEach(function (c, i) {
					if (c.name === card.name) {
						index = i;
					}
				});
				var cards = context.state.cards.slice();
				if (index !== -1) {
					cards.splice(index, 1, card);
				}
				context.commit("update_state", { cards: cards });
			},
			update_order_for_single_card: function (context, card) {
				// cache original order
				const _cards = context.state.cards.slice();
				const _columns = context.state.columns.slice();
				let args = {};
				let method_name = "";

				if (card.new) {
					method_name = "add_card";
					args = {
						board_name: context.state.board.name,
						docname: card.name,
						colname: card.colname,
					};
				} else {
					method_name = "update_order_for_single_card";
					args = {
						board_name: context.state.board.name,
						docname: card.name,
						from_colname: card.from_colname,
						to_colname: card.to_colname,
						old_index: card.old_index,
						new_index: card.new_index,
					};
				}
				frappe.dom.freeze();
				frappe
					.call({
						method: method_prefix + method_name,
						args: args,
						callback: (r) => {
							let board = r.message;
							let updated_cards = [
								{ name: card.name, column: card.to_colname || card.colname },
							];
							let cards = update_cards_column(updated_cards);
							let columns = prepare_columns(board.columns);
							context.commit("update_state", {
								cards: cards,
								columns: columns,
							});
							frappe.dom.unfreeze();
						},
					})
					.fail(function () {
						// revert original order
						context.commit("update_state", {
							cards: _cards,
							columns: _columns,
						});
						frappe.dom.unfreeze();
					});
			},
			update_order: function (context) {
				// cache original order
				const _cards = context.state.cards.slice();
				const _columns = context.state.columns.slice();

				const order = {};
				context.state.wrapper.find(".kanban-column[data-column-value]").each(function () {
					var col_name = $(this).data().columnValue;
					order[col_name] = [];
					$(this)
						.find(".kanban-card-wrapper")
						.each(function () {
							var card_name = decodeURIComponent($(this).data().name);
							order[col_name].push(card_name);
						});
				});

				frappe
					.call({
						method: method_prefix + "update_order",
						args: {
							board_name: context.state.board.name,
							order: order,
						},
						callback: (r) => {
							var board = r.message[0];
							var updated_cards = r.message[1];
							var cards = update_cards_column(updated_cards);
							var columns = prepare_columns(board.columns);
							context.commit("update_state", {
								cards: cards,
								columns: columns,
							});
						},
					})
					.fail(function () {
						// revert original order
						context.commit("update_state", {
							cards: _cards,
							columns: _columns,
						});
					});
			},
			update_column_order: function (context, order) {
				return frappe
					.call({
						method: method_prefix + "update_column_order",
						args: {
							board_name: context.state.board.name,
							order: order,
						},
					})
					.then(function (r) {
						var board = r.message;
						var columns = prepare_columns(board.columns);
						context.commit("update_state", {
							columns: columns,
						});
					});
			},
			set_indicator: function (context, { column, color }) {
				return frappe
					.call({
						method: method_prefix + "set_indicator",
						args: {
							board_name: context.state.board.name,
							column_name: column.title,
							indicator: color,
						},
					})
					.then(function (r) {
						var board = r.message;
						var columns = prepare_columns(board.columns);
						context.commit("update_state", {
							columns: columns,
						});
					});
			},
		},
	});

	frappe.views.KanbanBoard = function (opts) {
		var self = {};
		self.wrapper = opts.wrapper;
		self.cur_list = opts.cur_list;
		self.board_name = opts.board_name;
		self.board_perms = self.cur_list.board_perms;

		self.update = function (cards) {
			// update cards internally
			opts.cards = cards;

			if (self.wrapper.find(".kanban").length > 0 && self.cur_list.start !== 0) {
				store.dispatch("update_cards", cards);
			} else {
				init();
			}
		};

		function init() {
			store.dispatch("init", opts);
			columns_unwatcher && columns_unwatcher();
			store.watch((state, getters) => {
				return state.columns;
			}, make_columns);
			prepare();
			make_columns();
			store.watch((state, getters) => {
				return state.cur_list;
			}, setup_restore_columns);
			columns_unwatcher = store.watch((state, getters) => {
				return state.columns;
			}, setup_restore_columns);
			store.watch((state, getters) => {
				return state.empty_state;
			}, show_empty_state);

			store.dispatch("update_order");
		}

		function prepare() {
			self.$kanban_board = self.wrapper.find(".kanban");

			if (self.$kanban_board.length === 0) {
				self.$kanban_board = $(frappe.render_template("kanban_board"));
				self.$kanban_board.appendTo(self.wrapper);
			}

			self.$filter_area = self.cur_list.$page.find(".active-tag-filters");
			bind_events();
			setup_sortable();
		}

		function make_columns() {
			self.$kanban_board.find(".kanban-column").not(".add-new-column").remove();
			var columns = store.state.columns;

			columns.filter(is_active_column).map(function (col) {
				frappe.views.KanbanBoardColumn(col, self.$kanban_board, self.board_perms);
			});
		}

		function bind_events() {
			bind_add_column();
			bind_clickdrag();
		}

		function setup_sortable() {
			// If no write access to board, editing board (by dragging column) should be blocked
			if (!self.board_perms.write) return;

			var sortable = new Sortable(self.$kanban_board.get(0), {
				group: "columns",
				animation: 150,
				dataIdAttr: "data-column-value",
				filter: ".add-new-column",
				handle: ".kanban-column-title",
				onEnd: function () {
					var order = sortable.toArray();
					order = order.slice(1);
					store.dispatch("update_column_order", order);
				},
			});
		}

		function bind_add_column() {
			if (!self.board_perms.write) {
				// If no write access to board, editing board (by adding column) should be blocked
				self.$kanban_board.find(".add-new-column").remove();
				return;
			}

			var $add_new_column = self.$kanban_board.find(".add-new-column"),
				$compose_column = $add_new_column.find(".compose-column"),
				$compose_column_form = $add_new_column.find(".compose-column-form").hide();

			$compose_column.on("click", function () {
				$(this).hide();
				$compose_column_form.show();
				$compose_column_form.find("input").focus();
			});

			//save on enter
			$compose_column_form.keydown(function (e) {
				if (e.which == 13) {
					e.preventDefault();
					if (!frappe.request.ajax_count) {
						// not already working -- double entry
						var title = $compose_column_form.serializeArray()[0].value;
						var col = {
							title: title.trim(),
						};
						store.dispatch("add_column", col);
						$compose_column_form.find("input").val("");
						$compose_column.show();
						$compose_column_form.hide();
					}
				}
			});

			// on form blur
			$compose_column_form.find("input").on("blur", function () {
				$(this).val("");
				$compose_column.show();
				$compose_column_form.hide();
			});
		}

		function bind_clickdrag() {
			let isDown = false;
			let startX;
			let scrollLeft;
			let draggable = self.$kanban_board[0];

			draggable.addEventListener("mousedown", (e) => {
				// don't trigger scroll if one of the ancestors of the
				// clicked element matches any of these selectors
				let ignoreEl = [
					".kanban-column .kanban-column-header",
					".kanban-column .add-card",
					".kanban-column .kanban-card.new-card-area",
					".kanban-card-wrapper",
				];
				if (ignoreEl.some((el) => e.target.closest(el))) return;

				isDown = true;
				draggable.classList.add("clickdrag-active");
				startX = e.pageX - draggable.offsetLeft;
				scrollLeft = draggable.scrollLeft;
			});
			draggable.addEventListener("mouseleave", () => {
				isDown = false;
				draggable.classList.remove("clickdrag-active");
			});
			draggable.addEventListener("mouseup", () => {
				isDown = false;
				draggable.classList.remove("clickdrag-active");
			});
			draggable.addEventListener("mousemove", (e) => {
				if (!isDown) return;
				e.preventDefault();
				const x = e.pageX - draggable.offsetLeft;
				const walk = x - startX;
				draggable.scrollLeft = scrollLeft - walk;
			});
		}

		function setup_restore_columns() {
			var cur_list = store.state.cur_list;
			var columns = store.state.columns;
			var list_row_right = cur_list.$page
				.find(`[data-list-renderer='Kanban'] .list-row-right`)
				.css("margin-right", "15px");
			list_row_right.empty();

			var archived_columns = columns.filter(function (col) {
				return col.status === "Archived";
			});

			if (!archived_columns.length) return;

			var options = archived_columns.reduce(function (a, b) {
				return (
					a +
					`<li><a class='option'>" +
					"<span class='ellipsis' style='max-width: 100px; display: inline-block'>" +
					__(b.title) + "</span>" +
					"<button style='float:right;' data-column='" + b.title +
					"' class='btn btn-default btn-xs restore-column text-muted'>"
					+ __('Restore') + "</button></a></li>`
				);
			}, "");
			var $dropdown = $(
				"<div class='dropdown pull-right'>" +
					"<a class='text-muted dropdown-toggle' data-toggle='dropdown'>" +
					"<span class='dropdown-text'>" +
					__("Archived Columns") +
					"</span><i class='caret'></i></a>" +
					"<ul class='dropdown-menu'>" +
					options +
					"</ul>" +
					"</div>"
			);

			list_row_right.html($dropdown);

			$dropdown.find(".dropdown-menu").on("click", "button.restore-column", function () {
				var column_title = $(this).data().column;
				var col = {
					title: column_title,
					status: "Archived",
				};
				store.dispatch("restore_column", col);
			});
		}

		function show_empty_state() {
			var empty_state = store.state.empty_state;

			if (empty_state) {
				self.$kanban_board.find(".kanban-column").hide();
				self.$kanban_board.find(".kanban-empty-state").show();
			} else {
				self.$kanban_board.find(".kanban-column").show();
				self.$kanban_board.find(".kanban-empty-state").hide();
			}
		}

		init();

		return self;
	};

	frappe.views.KanbanBoardColumn = function (column, wrapper, board_perms) {
		var self = {};
		var filtered_cards = [];

		function init() {
			make_dom();
			setup_sortable();
			make_cards();
			store.watch((state, getters) => {
				return state.cards;
			}, make_cards);
			bind_add_card();
			bind_options();
		}

		function make_dom() {
			self.$kanban_column = $(
				frappe.render_template("kanban_column", {
					title: column.title,
					doctype: store.state.doctype,
					indicator: frappe.scrub(column.indicator, "-"),
				})
			).appendTo(wrapper);
			// add task, archive
			self.$kanban_cards = self.$kanban_column.find(".kanban-cards");
		}

		function make_cards() {
			self.$kanban_cards.empty();
			var cards = store.state.cards;
			filtered_cards = get_cards_for_column(cards, column);
			var filtered_cards_names = filtered_cards.map((card) => card.name);

			var order = column.order;
			if (order) {
				order = JSON.parse(order);
				// new cards
				filtered_cards.forEach(function (card) {
					if (order.indexOf(card.name) === -1) {
						frappe.views.KanbanBoardCard(card, self.$kanban_cards);
					}
				});
				order.forEach(function (name) {
					if (!filtered_cards_names.includes(name)) return;
					frappe.views.KanbanBoardCard(get_card(name), self.$kanban_cards);
				});
			} else {
				filtered_cards.map(function (card) {
					frappe.views.KanbanBoardCard(card, self.$kanban_cards);
				});
			}
		}

		function setup_sortable() {
			// Block card dragging/record editing without 'write' access to reference doctype
			if (!frappe.model.can_write(store.state.doctype)) return;

			Sortable.create(self.$kanban_cards.get(0), {
				group: "cards",
				animation: 150,
				dataIdAttr: "data-name",
				forceFallback: true,
				onStart: function () {
					wrapper.find(".kanban-card.add-card").fadeOut(200, function () {
						wrapper.find(".kanban-cards").height("100vh");
					});
				},
				onEnd: function (e) {
					wrapper.find(".kanban-card.add-card").fadeIn(100);
					wrapper.find(".kanban-cards").height("auto");
					// update order
					const args = {
						name: decodeURIComponent($(e.item).attr("data-name")),
						from_colname: $(e.from)
							.parents(".kanban-column")
							.attr("data-column-value"),
						to_colname: $(e.to).parents(".kanban-column").attr("data-column-value"),
						old_index: e.oldIndex,
						new_index: e.newIndex,
					};
					store.dispatch("update_order_for_single_card", args);
				},
				onAdd: function () {},
			});
		}

		function bind_add_card() {
			var $wrapper = self.$kanban_column;
			var $btn_add = $wrapper.find(".add-card");
			var $new_card_area = $wrapper.find(".new-card-area");

			if (!frappe.model.can_create(store.state.doctype)) {
				// Block record/card creation without 'create' access to reference doctype
				$btn_add.remove();
				$new_card_area.remove();
				return;
			}

			var $textarea = $new_card_area.find("textarea");

			//Add card button
			$new_card_area.hide();
			$btn_add.on("click", function () {
				$btn_add.hide();
				$new_card_area.show();
				$textarea.focus();
			});

			//save on enter
			$new_card_area.keydown(function (e) {
				if (e.which == 13) {
					e.preventDefault();
					if (!frappe.request.ajax_count) {
						// not already working -- double entry
						e.preventDefault();
						var card_title = $textarea.val();
						$new_card_area.hide();
						$textarea.val("");
						store
							.dispatch("add_card", {
								card_title,
								column_title: column.title,
							})
							.then(() => {
								$btn_add.show();
							});
					}
				}
			});

			// on textarea blur
			$textarea.on("blur", function () {
				$(this).val("");
				$btn_add.show();
				$new_card_area.hide();
			});
		}

		function bind_options() {
			if (!board_perms.write) {
				// If no write access to board, column options should be hidden
				self.$kanban_column.find(".column-options").remove();
				return;
			}

			self.$kanban_column
				.find(".column-options .dropdown-menu")
				.on("click", "[data-action]", function () {
					var $btn = $(this);
					var action = $btn.data().action;

					if (action === "archive") {
						store.dispatch("archive_column", column);
					} else if (action === "indicator") {
						var color = $btn.data().indicator;
						store.dispatch("set_indicator", { column, color });
					}
				});

			get_column_indicators(function (indicators) {
				let html = `<li class="button-group">${indicators
					.map((indicator) => {
						let classname = frappe.scrub(indicator, "-");
						return `<div data-action="indicator" data-indicator="${indicator}" class="btn btn-default btn-xs indicator-pill ${classname}"></div>`;
					})
					.join("")}</li>`;
				self.$kanban_column.find(".column-options .dropdown-menu").append(html);
			});
		}

		init();
	};

	frappe.views.KanbanBoardCard = function (card, wrapper) {
		var self = {};

		function init() {
			if (!card) return;
			make_dom();
			render_card_meta();
		}

		function make_dom() {
			var opts = {
				name: card.name,
				title: frappe.utils.html2text(card.title),
				disable_click: card._disable_click ? "disable-click" : "",
				creation: card.creation,
				doc_content: get_doc_content(card),
				image_url: cur_list.get_image_url(card),
				form_link: frappe.utils.get_form_link(card.doctype, card.name),
			};

			self.$card = $(frappe.render_template("kanban_card", opts)).appendTo(wrapper);

			if (!frappe.model.can_write(card.doctype)) {
				// Undraggable card without 'write' access to reference doctype
				self.$card.find(".kanban-card-body").css("cursor", "default");
			}
		}

		function get_doc_content(card) {
			let fields = [];
			for (let field_name of cur_list.board.fields) {
				let field =
					frappe.meta.docfield_map[card.doctype]?.[field_name] ||
					frappe.model.get_std_field(field_name);
				let label = cur_list.board.show_labels
					? `<span>${__(field.label, null, field.parent)}: </span>`
					: "";
				let value = frappe.format(card.doc[field_name], field);
				fields.push(`
					<div class="text-muted text-truncate">
						${label}
						<span>${value}</span>
					</div>
				`);
			}

			return fields.join("");
		}

		function get_tags_html(card) {
			return card.tags
				? `<div class="kanban-tags">
					${cur_list.get_tags_html(card.tags, 3, true)}
				</div>`
				: "";
		}

		function render_card_meta() {
			let html = get_tags_html(card);

			if (card.comment_count > 0)
				html += `<span class="list-comment-count small text-muted ">
					${frappe.utils.icon("es-line-chat-alt")}
					${card.comment_count}
				</span>`;

			const $assignees_group = get_assignees_group();

			html += `
				<span class="kanban-assignments"></span>
				${cur_list.get_like_html(card)}
			`;

			if (card.color && frappe.ui.color.validate_hex(card.color)) {
				const $div = $("<div>");
				$("<div></div>")
					.css({
						width: "30px",
						height: "4px",
						borderRadius: "2px",
						marginBottom: "8px",
						backgroundColor: card.color,
					})
					.appendTo($div);

				self.$card.find(".kanban-card .kanban-title-area").prepend($div);
			}

			self.$card
				.find(".kanban-card-meta")
				.empty()
				.append(html)
				.find(".kanban-assignments")
				.append($assignees_group);
		}

		function get_assignees_group() {
			return frappe.avatar_group(card.assigned_list, 3, {
				css_class: "avatar avatar-small",
				action_icon: "add",
				action: show_assign_to_dialog,
			});
		}

		function show_assign_to_dialog(e) {
			e.preventDefault();
			e.stopPropagation();
			self.assign_to = new frappe.ui.form.AssignToDialog({
				obj: self,
				method: "frappe.desk.form.assign_to.add",
				doctype: card.doctype,
				docname: card.name,
				callback: function () {
					const users = self.assign_to_dialog.get_values().assign_to;
					card.assigned_list = [...new Set(card.assigned_list.concat(users))];
					store.dispatch("update_card", card);
				},
			});
			self.assign_to_dialog = self.assign_to.dialog;
			self.assign_to_dialog.show();
		}

		init();
	};

	function prepare_card(card, state, doc) {
		var assigned_list = card._assign ? JSON.parse(card._assign) : [];
		var comment_count = card._comment_count || 0;

		if (doc) {
			card = Object.assign({}, card, doc);
		}

		return {
			doctype: state.doctype,
			name: card.name,
			title: card[state.card_meta.title_field.fieldname],
			creation: moment(card.creation).format("MMM DD, YYYY"),
			_liked_by: card._liked_by,
			image: card[cur_list.meta.image_field],
			tags: card._user_tags,
			column: card[state.board.field_name],
			assigned_list: card.assigned_list || assigned_list,
			comment_count: card.comment_count || comment_count,
			color: card.color || null,
			doc: doc || card,
		};
	}

	function prepare_columns(columns) {
		return columns.map(function (col) {
			return {
				title: col.column_name,
				status: col.status,
				order: col.order,
				indicator: col.indicator || "gray",
			};
		});
	}

	function modify_column_field_in_c11n(doc, board, title, action) {
		doc.fields.forEach(function (df) {
			if (df.fieldname === board.field_name && df.fieldtype === "Select") {
				if (!df.options) df.options = "";

				if (action === "add") {
					//add column_name to Select field's option field
					if (!df.options.includes(title)) df.options += "\n" + title;
				} else if (action === "delete") {
					var options = df.options.split("\n");
					var index = options.indexOf(title);
					if (index !== -1) options.splice(index, 1);
					df.options = options.join("\n");
				}
			}
		});
		return doc;
	}

	function fetch_customization(doctype) {
		return new Promise(function (resolve) {
			frappe.model.with_doc("Customize Form", "Customize Form", function () {
				var doc = frappe.get_doc("Customize Form");
				doc.doc_type = doctype;
				frappe.call({
					doc: doc,
					method: "fetch_to_customize",
					callback: function (r) {
						resolve(r.docs[0]);
					},
				});
			});
		});
	}

	function save_customization(doc) {
		if (!doc) return;
		doc.hide_success = true;
		return frappe.call({
			doc: doc,
			method: "save_customization",
		});
	}

	function insert_doc(doc) {
		return frappe.call({
			method: "frappe.client.insert",
			args: {
				doc: doc,
			},
			callback: function () {
				frappe.model.clear_doc(doc.doctype, doc.name);
				frappe.show_alert({ message: __("Saved"), indicator: "green" }, 1);
			},
		});
	}

	function update_kanban_board(board_name, column_title, action) {
		var method;
		var args = {
			board_name: board_name,
			column_title: column_title,
		};
		if (action === "add") {
			method = "add_column";
		} else if (action === "archive" || action === "restore") {
			method = "archive_restore_column";
			args.status = action === "archive" ? "Archived" : "Active";
		}
		return frappe.call({
			method: method_prefix + method,
			args: args,
		});
	}

	function is_active_column(col) {
		return col.status !== "Archived";
	}

	function get_cards_for_column(cards, column) {
		return cards.filter(function (card) {
			return card.column === column.title;
		});
	}

	function get_card(name) {
		return store.state.cards.find(function (c) {
			return c.name === name;
		});
	}

	function update_cards_column(updated_cards) {
		var cards = store.state.cards;
		cards.forEach(function (c) {
			updated_cards.forEach(function (uc) {
				if (uc.name === c.name) {
					c.column = uc.column;
				}
			});
		});
		return cards;
	}

	function get_column_indicators(callback) {
		frappe.model.with_doctype("Kanban Board Column", function () {
			var meta = frappe.get_meta("Kanban Board Column");
			var indicators;
			meta.fields.forEach(function (df) {
				if (df.fieldname === "indicator") {
					indicators = df.options.split("\n");
				}
			});
			if (!indicators) {
				//
				indicators = ["green", "blue", "orange", "gray"];
			}
			callback(indicators);
		});
	}
})();

Frappe List View Implmentation on top of which Kanban is implemented: basepath: frappe/frappe/public/js/frappe/list ./base_list.js

frappe.provide("frappe.views");

frappe.views.BaseList = class BaseList {
	constructor(opts) {
		Object.assign(this, opts);
	}

	show() {
		return frappe.run_serially([
			() => this.show_skeleton(),
			() => this.fetch_meta(),
			() => this.hide_skeleton(),
			() => this.check_permissions(),
			() => this.init(),
			() => this.before_refresh(),
			() => this.refresh(),
		]);
	}

	init() {
		if (this.init_promise) return this.init_promise;

		let tasks = [
			this.setup_defaults,
			this.set_stats,
			this.setup_fields,
			// make view
			this.setup_page,
			this.setup_side_bar,
			this.setup_main_section,
			this.setup_view,
			this.setup_view_menu,
		].map((fn) => fn.bind(this));

		this.init_promise = frappe.run_serially(tasks);
		return this.init_promise;
	}

	setup_defaults() {
		this.page_name = frappe.get_route_str();
		this.page_title = this.page_title || frappe.router.doctype_layout || __(this.doctype);
		this.meta = frappe.get_meta(this.doctype);
		this.settings = frappe.listview_settings[this.doctype] || {};
		this.user_settings = frappe.get_user_settings(this.doctype);

		this.start = 0;
		this.page_length = frappe.is_large_screen() ? 100 : 20;
		this.data = [];
		this.method = "frappe.desk.reportview.get";

		this.can_create = frappe.model.can_create(this.doctype);
		this.can_write = frappe.model.can_write(this.doctype);

		this.fields = [];
		this.filters = [];
		this.sort_by = this.meta.sort_field || "modified";
		this.sort_order = this.meta.sort_order || "desc";

		// Setup buttons
		this.primary_action = null;
		this.secondary_action = null;

		this.menu_items = [
			{
				label: __("Refresh"),
				action: () => this.refresh(),
				class: "visible-xs",
			},
		];
	}

	get_list_view_settings() {
		return frappe
			.call("frappe.desk.listview.get_list_settings", {
				doctype: this.doctype,
			})
			.then((doc) => (this.list_view_settings = doc.message || {}));
	}

	async setup_fields() {
		await this.set_fields();
		this.build_fields();
	}

	async set_fields() {
		let fields = [].concat(frappe.model.std_fields_list, this.meta.title_field);

		fields.forEach((f) => this._add_field(f));
	}

	get_fields_in_list_view() {
		return this.meta.fields.filter((df) => {
			return (
				(frappe.model.is_value_type(df.fieldtype) &&
					df.in_list_view &&
					frappe.perm.has_perm(this.doctype, df.permlevel, "read")) ||
				(df.fieldtype === "Currency" && df.options && !df.options.includes(":")) ||
				df.fieldname === "status"
			);
		});
	}

	build_fields() {
		// fill in missing doctype
		this.fields = this.fields.map((f) => {
			if (typeof f === "string") {
				f = [f, this.doctype];
			}
			return f;
		});
		// remove null or undefined values
		this.fields = this.fields.filter(Boolean);
		//de-duplicate
		this.fields = this.fields.uniqBy((f) => f[0] + f[1]);
	}

	_add_field(fieldname, doctype) {
		if (!fieldname) return;

		if (!doctype) doctype = this.doctype;

		if (typeof fieldname === "object") {
			// df is passed
			const df = fieldname;
			fieldname = df.fieldname;
			doctype = df.parent || doctype;
		}

		if (!this.fields) this.fields = [];
		const is_valid_field =
			frappe.model.std_fields_list.includes(fieldname) ||
			frappe.meta.has_field(doctype, fieldname) ||
			fieldname === "_seen";

		let is_virtual = this.meta.fields.find((df) => df.fieldname == fieldname)?.is_virtual;

		if (!is_valid_field || is_virtual) {
			return;
		}

		this.fields.push([fieldname, doctype]);
	}

	set_stats() {
		this.stats = ["_user_tags"];
		// add workflow field (as priority)
		this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype);
		if (this.workflow_state_fieldname) {
			if (!frappe.workflow.workflows[this.doctype]["override_status"]) {
				this._add_field(this.workflow_state_fieldname);
			}
			this.stats.push(this.workflow_state_fieldname);
		}
	}

	fetch_meta() {
		return frappe.model.with_doctype(this.doctype);
	}

	show_skeleton() {}

	hide_skeleton() {}

	check_permissions() {
		return true;
	}

	setup_page() {
		this.page = this.parent.page;
		this.$page = $(this.parent);
		!this.hide_card_layout && this.page.main.addClass("frappe-card");
		this.page.page_form.removeClass("row").addClass("flex");
		this.hide_page_form && this.page.page_form.hide();
		this.hide_sidebar && this.$page.addClass("no-list-sidebar");
		this.setup_page_head();
	}

	setup_page_head() {
		this.set_title();
		this.set_menu_items();
		this.set_breadcrumbs();
	}

	set_title() {
		this.page.set_title(this.page_title, null, true, "", this.meta?.description);
	}

	setup_view_menu() {
		// TODO: add all icons
		const icon_map = {
			Image: "image-view",
			List: "list",
			Report: "small-file",
			Calendar: "calendar",
			Gantt: "gantt",
			Kanban: "kanban",
			Dashboard: "dashboard",
			Map: "map",
		};

		if (frappe.boot.desk_settings.view_switcher && !this.meta.force_re_route_to_default_view) {
			/* @preserve
			for translation, don't remove
			__("List View") __("Report View") __("Dashboard View") __("Gantt View"),
			__("Kanban View") __("Calendar View") __("Image View") __("Inbox View"),
			__("Tree View") __("Map View") */
			this.views_menu = this.page.add_custom_button_group(
				__("{0} View", [this.view_name]),
				icon_map[this.view_name] || "list"
			);
			this.views_list = new frappe.views.ListViewSelect({
				doctype: this.doctype,
				parent: this.views_menu,
				page: this.page,
				list_view: this,
				sidebar: this.list_sidebar,
				icon_map: icon_map,
			});
		}
	}

	set_default_secondary_action() {
		if (this.secondary_action) {
			const $secondary_action = this.page.set_secondary_action(
				this.secondary_action.label,
				this.secondary_action.action,
				this.secondary_action.icon
			);
			if (!this.secondary_action.icon) {
				$secondary_action.addClass("hidden-xs");
			} else if (!this.secondary_action.label) {
				$secondary_action.addClass("visible-xs");
			}
		} else {
			this.refresh_button = this.page.add_action_icon(
				"es-line-reload",
				() => {
					this.refresh();
				},
				"",
				__("Reload List")
			);
		}
	}

	set_menu_items() {
		this.set_default_secondary_action();

		this.menu_items &&
			this.menu_items.map((item) => {
				if (item.condition && item.condition() === false) {
					return;
				}
				const $item = this.page.add_menu_item(
					item.label,
					item.action,
					item.standard,
					item.shortcut
				);
				if (item.class) {
					$item && $item.addClass(item.class);
				}
			});
	}

	set_breadcrumbs() {
		frappe.breadcrumbs.add(this.meta.module, this.doctype);
	}

	setup_side_bar() {
		if (this.hide_sidebar || !frappe.boot.desk_settings.list_sidebar) return;
		this.list_sidebar = new frappe.views.ListSidebar({
			doctype: this.doctype,
			stats: this.stats,
			parent: this.$page.find(".layout-side-section"),
			page: this.page,
			list_view: this,
		});
	}

	toggle_side_bar(show) {
		let show_sidebar = show || JSON.parse(localStorage.show_sidebar || "true");
		show_sidebar = !show_sidebar;
		localStorage.show_sidebar = show_sidebar;
		this.show_or_hide_sidebar();
		$(document.body).trigger("toggleListSidebar");
	}

	show_or_hide_sidebar() {
		let show_sidebar = JSON.parse(localStorage.show_sidebar || "true");
		$(document.body).toggleClass("no-list-sidebar", !show_sidebar);
	}

	setup_main_section() {
		return frappe.run_serially(
			[
				this.setup_list_wrapper,
				this.show_or_hide_sidebar,
				this.setup_filter_area,
				this.setup_sort_selector,
				this.setup_result_area,
				this.setup_no_result_area,
				this.setup_freeze_area,
				this.setup_paging_area,
			].map((fn) => fn.bind(this))
		);
	}

	setup_list_wrapper() {
		this.$frappe_list = $('<div class="frappe-list">').appendTo(this.page.main);
	}

	setup_filter_area() {
		if (this.hide_filters) return;
		this.filter_area = new FilterArea(this);

		if (this.filters && this.filters.length > 0) {
			return this.filter_area.set(this.filters).catch(() => {
				this.filter_area.clear(false);
			});
		}
	}

	setup_sort_selector() {
		if (this.hide_sort_selector) return;
		this.sort_selector = new frappe.ui.SortSelector({
			parent: this.$filter_section,
			doctype: this.doctype,
			args: {
				sort_by: this.sort_by,
				sort_order: this.sort_order,
			},
			onchange: this.on_sort_change.bind(this),
		});
	}

	on_sort_change() {
		this.refresh();
	}

	setup_result_area() {
		this.$result = $(`<div class="result">`);
		this.$frappe_list.append(this.$result);
	}

	setup_no_result_area() {
		this.$no_result = $(`
			<div class="no-result text-muted flex justify-center align-center">
				${this.get_no_result_message()}
			</div>
		`).hide();
		this.$frappe_list.append(this.$no_result);
	}

	setup_freeze_area() {
		this.$freeze = $('<div class="freeze"></div>').hide();
		this.$frappe_list.append(this.$freeze);
	}

	get_no_result_message() {
		return __("Nothing to show");
	}

	setup_paging_area() {
		const paging_values = [20, 100, 500, 2500];
		this.$paging_area = $(
			`<div class="list-paging-area level">
				<div class="level-left">
					<div class="btn-group">
						${paging_values
							.map(
								(value) => `
							<button type="button" class="btn btn-default btn-sm btn-paging"
								data-value="${value}">
								${value}
							</button>
						`
							)
							.join("")}
					</div>
				</div>
				<div class="level-right">
					<button class="btn btn-default btn-more btn-sm">
						${__("Load More")}
					</button>
				</div>
			</div>`
		).hide();
		this.$frappe_list.append(this.$paging_area);

		// set default paging btn active
		this.$paging_area
			.find(`.btn-paging[data-value="${this.page_length}"]`)
			.addClass("btn-info");

		this.$paging_area.on("click", ".btn-paging", (e) => {
			const $this = $(e.currentTarget);

			// set active button
			this.$paging_area.find(".btn-paging").removeClass("btn-info");
			$this.addClass("btn-info");

			this.start = 0;
			this.page_length = this.selected_page_count = $this.data().value;

			this.refresh();
		});

		this.$paging_area.on("click", ".btn-more", (e) => {
			this.start += this.page_length;
			this.page_length = this.selected_page_count || 20;
			this.refresh();
		});
	}

	get_fields() {
		// convert [fieldname, Doctype] => tabDoctype.fieldname
		return this.fields.map((f) => frappe.model.get_full_column_name(f[0], f[1]));
	}

	get_group_by() {
		let name_field = this.fields && this.fields.find((f) => f[0] == "name");
		if (name_field) {
			return frappe.model.get_full_column_name(name_field[0], name_field[1]);
		}
		return null;
	}

	setup_view() {
		// for child classes
	}

	get_filter_value(fieldname) {
		const filter = this.get_filters_for_args().filter((f) => f[1] == fieldname)[0];
		if (!filter) return;
		return (
			{
				like: filter[3]?.replace(/^%?|%$/g, ""),
				"not set": null,
			}[filter[2]] || filter[3]
		);
	}

	get_filters_for_args() {
		// filters might have a fifth param called hidden,
		// we don't want to pass that server side
		return this.filter_area ? this.filter_area.get().map((filter) => filter.slice(0, 4)) : [];
	}

	get_args() {
		let filters = this.get_filters_for_args();
		let group_by = this.get_group_by();
		let group_by_required =
			Array.isArray(filters) &&
			filters.some((filter) => {
				return filter[0] !== this.doctype;
			});
		return {
			doctype: this.doctype,
			fields: this.get_fields(),
			filters,
			order_by: this.sort_selector && this.sort_selector.get_sql_string(),
			start: this.start,
			page_length: this.page_length,
			view: this.view,
			group_by: group_by_required ? group_by : null,
		};
	}

	get_call_args() {
		const args = this.get_args();
		return {
			method: this.method,
			args: args,
			freeze: this.freeze_on_refresh || false,
			freeze_message: this.freeze_message || __("Loading") + "...",
		};
	}

	before_refresh() {
		// modify args here just before making the request
		// see list_view.js
	}

	refresh() {
		let args = this.get_call_args();
		if (this.no_change(args)) {
			// console.log('throttled');
			return Promise.resolve();
		}
		this.freeze(true);
		// fetch data from server
		return frappe.call(args).then((r) => {
			// render
			this.prepare_data(r);
			this.toggle_result_area();
			this.before_render();
			this.render();
			this.after_render();
			this.freeze(false);
			this.reset_defaults();
			if (this.settings.refresh) {
				this.settings.refresh(this);
			}
		});
	}

	no_change(args) {
		// returns true if arguments are same for the last 3 seconds
		// this helps in throttling if called from various sources
		if (this.last_args && JSON.stringify(args) === this.last_args) {
			return true;
		}
		this.last_args = JSON.stringify(args);
		setTimeout(() => {
			this.last_args = null;
		}, 3000);
		return false;
	}

	prepare_data(r) {
		let data = r.message || {};

		// extract user_info for assignments
		Object.assign(frappe.boot.user_info, data.user_info);
		delete data.user_info;

		data = !Array.isArray(data) ? frappe.utils.dict(data.keys, data.values) : data;

		if (this.start === 0) {
			this.data = data;
		} else {
			this.data = this.data.concat(data);
		}

		this.data = this.data.uniqBy((d) => d.name);
	}

	reset_defaults() {
		this.page_length = this.page_length + this.start;
		this.start = 0;
	}

	freeze() {
		// show a freeze message while data is loading
	}

	before_render() {}

	after_render() {}

	render() {
		// for child classes
	}

	on_filter_change() {
		// fired when filters are added or removed
	}

	toggle_result_area() {
		this.$result.toggle(this.data.length > 0);
		this.$paging_area.toggle(this.data.length > 0);
		this.$no_result.toggle(this.data.length == 0);

		const show_more = this.start + this.page_length <= this.data.length;
		this.$paging_area.find(".btn-more").toggle(show_more);
	}

	call_for_selected_items(method, args = {}) {
		args.names = this.get_checked_items(true);

		frappe.call({
			method: method,
			args: args,
			freeze: true,
			callback: (r) => {
				if (!r.exc) {
					this.refresh();
				}
			},
		});
	}
};

class FilterArea {
	constructor(list_view) {
		this.list_view = list_view;
		this.list_view.page.page_form.append(`<div class="standard-filter-section flex"></div>`);

		const filter_area = this.list_view.hide_page_form
			? this.list_view.page.custom_actions
			: this.list_view.page.page_form;

		this.list_view.$filter_section = $('<div class="filter-section flex">').appendTo(
			filter_area
		);

		this.$filter_list_wrapper = this.list_view.$filter_section;
		this.trigger_refresh = true;

		this.debounced_refresh_list_view = frappe.utils.debounce(
			this.refresh_list_view.bind(this),
			300
		);
		this.setup();
	}

	setup() {
		if (!this.list_view.hide_page_form) this.make_standard_filters();
		this.make_filter_list();
	}

	get() {
		let filters = this.filter_list.get_filters();
		let standard_filters = this.get_standard_filters();

		return filters.concat(standard_filters).uniqBy(JSON.stringify);
	}

	set(filters) {
		// use to method to set filters without triggering refresh
		this.trigger_refresh = false;
		return this.add(filters, false).then(() => {
			this.trigger_refresh = true;
			this.filter_list.update_filter_button();
		});
	}

	add(filters, refresh = true) {
		if (!filters || (Array.isArray(filters) && filters.length === 0)) return Promise.resolve();

		if (typeof filters[0] === "string") {
			// passed in the format of doctype, field, condition, value
			const filter = Array.from(arguments);
			filters = [filter];
		}

		filters = filters.filter((f) => !this.exists(f));

		// standard filters = filters visible on list view
		// non-standard filters = filters set by filter button
		const { non_standard_filters, promise } = this.set_standard_filter(filters);

		return promise
			.then(() => {
				return (
					non_standard_filters.length > 0 &&
					this.filter_list.add_filters(non_standard_filters)
				);
			})
			.then(() => {
				refresh && this.list_view.refresh();
			});
	}

	refresh_list_view() {
		if (this.trigger_refresh) {
			this.list_view.start = 0;
			this.list_view.refresh();
			this.list_view.on_filter_change();
		}
	}

	exists(f) {
		let exists = false;
		// check in standard filters
		const fields_dict = this.list_view.page.fields_dict;
		if (f[2] === "=" && f[1] in fields_dict) {
			const value = fields_dict[f[1]].get_value();
			if (value) {
				exists = true;
			}
		}

		// check in filter area
		if (!exists) {
			exists = this.filter_list.filter_exists(f);
		}

		return exists;
	}

	set_standard_filter(filters) {
		if (filters.length === 0) {
			return {
				non_standard_filters: [],
				promise: Promise.resolve(),
			};
		}

		const fields_dict = this.list_view.page.fields_dict;

		return filters.reduce((out, filter) => {
			const [dt, fieldname, condition, value] = filter;
			out.promise = out.promise || Promise.resolve();
			out.non_standard_filters = out.non_standard_filters || [];

			// set in list view area if filters are present
			// don't set like filter on link fields (gets reset)
			if (
				fields_dict[fieldname] &&
				(condition === "=" ||
					(condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link") ||
					(condition === "descendants of (inclusive)" &&
						fields_dict[fieldname]?.df?.fieldtype == "Link"))
			) {
				// standard filter
				out.promise = out.promise.then(() => fields_dict[fieldname].set_value(value));
			} else {
				// filter out non standard filters
				out.non_standard_filters.push(filter);
			}
			return out;
		}, {});
	}

	remove_filters(filters) {
		filters.map((f) => {
			this.remove(f[1]);
		});
	}

	remove(fieldname) {
		const fields_dict = this.list_view.page.fields_dict;

		if (fieldname in fields_dict) {
			fields_dict[fieldname].set_value("");
		}

		let filter = this.filter_list.get_filter(fieldname);
		if (filter) filter.remove();
		this.filter_list.apply();
		return Promise.resolve();
	}

	clear(refresh = true) {
		if (!refresh) {
			this.trigger_refresh = false;
		}

		this.filter_list.clear_filters();

		const promises = [];
		const fields_dict = this.list_view.page.fields_dict;
		for (let key in fields_dict) {
			const field = this.list_view.page.fields_dict[key];
			promises.push(() => field.set_value(""));
		}
		return frappe.run_serially(promises).then(() => {
			this.trigger_refresh = true;
		});
	}

	make_standard_filters() {
		this.standard_filters_wrapper = this.list_view.page.page_form.find(
			".standard-filter-section"
		);
		let fields = [];

		if (!this.list_view.settings.hide_name_filter) {
			fields.push({
				fieldtype: "Data",
				label: "ID",
				condition: "like",
				fieldname: "name",
				onchange: () => this.debounced_refresh_list_view(),
			});
		}

		if (this.list_view.custom_filter_configs) {
			this.list_view.custom_filter_configs.forEach((config) => {
				config.onchange = () => this.debounced_refresh_list_view();
			});

			fields = fields.concat(this.list_view.custom_filter_configs);
		}

		const doctype_fields = this.list_view.meta.fields;
		const title_field = this.list_view.meta.title_field;

		fields = fields.concat(
			doctype_fields
				.filter(
					(df) =>
						df.fieldname === title_field ||
						(df.in_standard_filter && frappe.model.is_value_type(df.fieldtype))
				)
				.map((df) => {
					let options = df.options;
					let condition = "=";
					let fieldtype = df.fieldtype;
					if (
						[
							"Text",
							"Small Text",
							"Text Editor",
							"HTML Editor",
							"Data",
							"Code",
							"Phone",
							"JSON",
							"Read Only",
						].includes(fieldtype)
					) {
						fieldtype = "Data";
						condition = "like";
					}
					if (df.fieldtype == "Select" && df.options) {
						options = df.options.split("\n");
						if (options.length > 0 && options[0] != "") {
							options.unshift("");
							options = options.join("\n");
						}
					}
					if (
						df.fieldtype == "Link" &&
						df.options &&
						frappe.boot.treeviews.includes(df.options)
					) {
						condition = "descendants of (inclusive)";
					}

					return {
						fieldtype: fieldtype,
						label: __(df.label, null, df.parent),
						options: options,
						fieldname: df.fieldname,
						condition: condition,
						onchange: () => this.debounced_refresh_list_view(),
						ignore_link_validation: fieldtype === "Dynamic Link",
						is_filter: 1,
					};
				})
		);

		fields.map((df) => {
			this.list_view.page.add_field(df, this.standard_filters_wrapper);
		});
	}

	get_standard_filters() {
		const filters = [];
		const fields_dict = this.list_view.page.fields_dict;
		for (let key in fields_dict) {
			let field = fields_dict[key];
			let value = field.get_value();
			if (value) {
				if (field.df.condition === "like" && !value.includes("%")) {
					value = "%" + value + "%";
				}
				filters.push([
					field.df.doctype || this.list_view.doctype,
					field.df.fieldname,
					field.df.condition || "=",
					value,
				]);
			}
		}

		return filters;
	}

	make_filter_list() {
		$(`<div class="filter-selector">
			<div class="btn-group">
				<button class="btn btn-default btn-sm filter-button">
					<span class="filter-icon">
						${frappe.utils.icon("es-line-filter")}
					</span>
					<span class="button-label hidden-xs">
					${__("Filter")}
					<span>
				</button>
				<button class="btn btn-default btn-sm filter-x-button" title="${__("Clear all filters")}">
					<span class="filter-icon">
						${frappe.utils.icon("es-small-close")}
					</span>
				</button>
			</div>
		</div>`).appendTo(this.$filter_list_wrapper);

		this.filter_button = this.$filter_list_wrapper.find(".filter-button");
		this.filter_x_button = this.$filter_list_wrapper.find(".filter-x-button");
		this.filter_list = new frappe.ui.FilterGroup({
			base_list: this.list_view,
			parent: this.$filter_list_wrapper,
			doctype: this.list_view.doctype,
			filter_button: this.filter_button,
			filter_x_button: this.filter_x_button,
			default_filters: [],
			on_change: () => this.debounced_refresh_list_view(),
		});
	}

	is_being_edited() {
		// returns true if user is currently editing filters
		return (
			this.filter_list &&
			this.filter_list.wrapper &&
			this.filter_list.wrapper.find(".filter-box:visible").length > 0
		);
	}
}

// utility function to validate view modes
frappe.views.view_modes = [
	"List",
	"Report",
	"Dashboard",
	"Gantt",
	"Kanban",
	"Calendar",
	"Image",
	"Inbox",
	"Tree",
	"Map",
];
frappe.views.is_valid = (view_mode) => frappe.views.view_modes.includes(view_mode);

./list_view.js

import BulkOperations from "./bulk_operations";
import ListSettings from "./list_settings";

frappe.provide("frappe.views");

frappe.views.ListView = class ListView extends frappe.views.BaseList {
	static load_last_view() {
		const route = frappe.get_route();
		const doctype = route[1];

		if (route.length === 2) {
			const user_settings = frappe.get_user_settings(doctype);
			const last_view = user_settings.last_view;
			frappe.set_route(
				"list",
				frappe.router.doctype_layout || doctype,
				frappe.views.is_valid(last_view) ? last_view.toLowerCase() : "list"
			);
			return true;
		}
		return false;
	}

	constructor(opts) {
		super(opts);
		this.show();
		this.debounced_refresh = frappe.utils.debounce(
			this.process_document_refreshes.bind(this),
			2000
		);
		this.count_upper_bound = 1001;
		this._element_factory = new ElementFactory(this.doctype);
	}

	has_permissions() {
		return frappe.perm.has_perm(this.doctype, 0, "read");
	}

	show() {
		this.parent.disable_scroll_to_top = true;
		super.show();
	}

	check_permissions() {
		if (!this.has_permissions()) {
			frappe.set_route("");
			frappe.throw(__("Not permitted to view {0}", [this.doctype]));
		}
	}

	show_skeleton() {
		this.$list_skeleton = this.parent.page.container.find(".list-skeleton");
		if (!this.$list_skeleton.length) {
			this.$list_skeleton = $(`
				<div class="row list-skeleton">
					<div class="col-lg-2">
						<div class="list-skeleton-box"></div>
					</div>
					<div class="col">
						<div class="list-skeleton-box"></div>
					</div>
				</div>
			`);
			this.parent.page.container.find(".page-content").append(this.$list_skeleton);
		}
		this.parent.page.container.find(".layout-main").hide();
		this.$list_skeleton.show();
	}

	hide_skeleton() {
		this.$list_skeleton && this.$list_skeleton.hide();
		this.parent.page.container.find(".layout-main").show();
	}

	get view_name() {
		return "List";
	}

	get view_user_settings() {
		return this.user_settings[this.view_name] || {};
	}

	setup_defaults() {
		super.setup_defaults();

		this.view = "List";
		// initialize with saved order by
		this.sort_by = this.view_user_settings.sort_by || this.sort_by || "modified";
		this.sort_order = this.view_user_settings.sort_order || this.sort_order || "desc";

		// build menu items
		this.menu_items = this.menu_items.concat(this.get_menu_items());

		// set filters from view_user_settings or list_settings
		if (Array.isArray(this.view_user_settings.filters)) {
			// Priority 1: view_user_settings
			const saved_filters = this.view_user_settings.filters;
			this.filters = this.validate_filters(saved_filters);
		} else {
			// Priority 2: filters in listview_settings
			this.filters = (this.settings.filters || []).map((f) => {
				if (f.length === 3) {
					f = [this.doctype, f[0], f[1], f[2]];
				}
				return f;
			});
		}

		if (this.view_name == "List") this.toggle_paging = true;

		this.patch_refresh_and_load_lib();
		return this.get_list_view_settings();
	}

	on_sort_change(sort_by, sort_order) {
		this.sort_by = sort_by;
		this.sort_order = sort_order;
		super.on_sort_change();
	}

	validate_filters(filters) {
		let valid_fields = this.meta.fields.map((df) => df.fieldname);
		valid_fields = valid_fields.concat(frappe.model.std_fields_list);
		return filters.filter((f) => valid_fields.includes(f[1])).uniqBy((f) => f[1]);
	}

	setup_page() {
		this.parent.list_view = this;
		super.setup_page();
	}

	setup_page_head() {
		super.setup_page_head();
		this.set_primary_action();
		this.set_actions_menu_items();
	}

	set_actions_menu_items() {
		this.actions_menu_items = this.get_actions_menu_items();
		this.workflow_action_menu_items = this.get_workflow_action_menu_items();
		this.workflow_action_items = {};

		const actions = this.actions_menu_items.concat(this.workflow_action_menu_items);
		actions.forEach((item) => {
			const $item = this.page.add_actions_menu_item(item.label, item.action, item.standard);
			if (item.class) {
				$item.addClass(item.class);
			}
			if (item.is_workflow_action && $item) {
				// can be used to dynamically show or hide action
				this.workflow_action_items[item.name] = $item;
			}
		});
	}

	show_restricted_list_indicator_if_applicable() {
		const match_rules_list = frappe.perm.get_match_rules(this.doctype);
		if (match_rules_list.length) {
			this.restricted_list = $(
				`<button class="btn btn-xs restricted-button flex align-center">
					${frappe.utils.icon("restriction", "xs")}
				</button>`
			)
				.click(() => this.show_restrictions(match_rules_list))
				.appendTo(this.page.page_form);
		}
	}

	show_restrictions(match_rules_list = []) {
		frappe.msgprint(
			frappe.render_template("list_view_permission_restrictions", {
				condition_list: match_rules_list,
			}),
			__("Restrictions", null, "Title of message showing restrictions in list view")
		);
	}

	get_fields() {
		return super
			.get_fields()
			.concat(
				Object.entries(this.link_field_title_fields || {}).map(
					(entry) => entry.join(".") + " as " + entry.join("_")
				)
			);
	}

	async set_fields() {
		this.link_field_title_fields = {};
		let fields = [].concat(
			frappe.model.std_fields_list,
			this.get_fields_in_list_view(),
			[this.meta.title_field, this.meta.image_field],
			this.settings.add_fields || [],
			this.meta.track_seen ? "_seen" : null,
			this.sort_by,
			"enabled",
			"disabled",
			"color"
		);

		await Promise.all(
			fields.map((f) => {
				return new Promise((resolve) => {
					const df =
						typeof f === "string" ? frappe.meta.get_docfield(this.doctype, f) : f;
					if (
						df &&
						df.fieldtype == "Link" &&
						frappe.boot.link_title_doctypes.includes(df.options)
					) {
						frappe.model.with_doctype(df.options, () => {
							const meta = frappe.get_meta(df.options);
							if (meta.show_title_field_in_link) {
								this.link_field_title_fields[
									typeof f === "string" ? f : f.fieldname
								] = meta.title_field;
							}

							this._add_field(f);
							resolve();
						});
					} else {
						this._add_field(f);
						resolve();
					}
				});
			})
		);

		this.fields.forEach((f) => {
			const df = frappe.meta.get_docfield(f[1], f[0]);
			if (df && df.fieldtype === "Currency" && df.options && !df.options.includes(":")) {
				this._add_field(df.options);
			}
		});
	}

	patch_refresh_and_load_lib() {
		// throttle refresh for 1s
		this.refresh = this.refresh.bind(this);
		this.refresh = frappe.utils.throttle(this.refresh, 1000);
		this.load_lib = new Promise((resolve) => {
			if (this.required_libs) {
				frappe.require(this.required_libs, resolve);
			} else {
				resolve();
			}
		});
		// call refresh every 5 minutes
		const interval = 5 * 60 * 1000;
		setInterval(() => {
			// don't call if route is different
			if (frappe.get_route_str() === this.page_name) {
				this.refresh();
			}
		}, interval);
	}

	set_primary_action() {
		if (this.can_create && !frappe.boot.read_only) {
			const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype);

			// Better style would be __("Add {0}", [doctype_name], "Primary action in list view")
			// Keeping it like this to not disrupt existing translations
			const label = `${__("Add", null, "Primary action in list view")} ${doctype_name}`;
			this.page.set_primary_action(
				label,
				() => {
					if (this.settings.primary_action) {
						this.settings.primary_action();
					} else {
						this.make_new_doc();
					}
				},
				"add"
			);
		} else {
			this.page.clear_primary_action();
		}
	}

	make_new_doc() {
		const doctype = this.doctype;
		const options = {};
		this.filter_area.get().forEach((f) => {
			if (f[2] === "=" && frappe.model.is_non_std_field(f[1])) {
				options[f[1]] = f[3];
			}
		});
		frappe.new_doc(doctype, options);
	}

	setup_view() {
		this.setup_columns();
		this.render_header();
		this.render_skeleton();
		this.setup_events();
		this.settings.onload && this.settings.onload(this);
		this.show_restricted_list_indicator_if_applicable();
	}

	refresh_columns(meta, list_view_settings) {
		this.meta = meta;
		this.list_view_settings = list_view_settings;

		this.setup_columns();
		this.refresh(true);
	}

	refresh(refresh_header = false) {
		return super.refresh().then(() => {
			this.render_header(refresh_header);
			this.update_checkbox();
			this.update_url_with_filters();
			this.setup_realtime_updates();
		});
	}

	update_checkbox(target) {
		if (!this.$checkbox_actions) return;

		let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");

		if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {
			$check_all_checkbox.prop("checked", false);
		}

		$check_all_checkbox.prop("checked", this.$checks.length === this.data.length);
	}

	setup_freeze_area() {
		this.$freeze = $(
			`<div class="freeze flex justify-center align-center text-muted">
				${__("Loading")}...
			</div>`
		).hide();
		this.$result.append(this.$freeze);
	}

	setup_columns() {
		// setup columns for list view
		this.columns = [];

		const get_df = frappe.meta.get_docfield.bind(null, this.doctype);

		// 1st column: title_field or name
		if (this.meta.title_field) {
			this.columns.push({
				type: "Subject",
				df: get_df(this.meta.title_field),
			});
		} else {
			this.columns.push({
				type: "Subject",
				df: {
					label: __("ID"),
					fieldname: "name",
				},
			});
		}

		this.columns.push({
			type: "Tag",
		});

		// 2nd column: Status indicator
		if (frappe.has_indicator(this.doctype)) {
			// indicator
			this.columns.push({
				type: "Status",
			});
		}

		const fields_in_list_view = this.get_fields_in_list_view();
		// Add rest from in_list_view docfields
		this.columns = this.columns.concat(
			fields_in_list_view
				.filter((df) => {
					if (frappe.has_indicator(this.doctype) && df.fieldname === "status") {
						return false;
					}
					if (!df.in_list_view || df.is_virtual) {
						return false;
					}
					return df.fieldname !== this.meta.title_field;
				})
				.map((df) => ({
					type: "Field",
					df,
				}))
		);

		if (this.list_view_settings.fields) {
			this.columns = this.reorder_listview_fields();
		}

		// limit max to 8 columns if no total_fields is set in List View Settings
		// Screen with low density no of columns 4
		// Screen with medium density no of columns 6
		// Screen with high density no of columns 8
		let total_fields = 6;

		if (window.innerWidth <= 1366) {
			total_fields = 4;
		} else if (window.innerWidth >= 1920) {
			total_fields = 10;
		}

		this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields);

		if (
			!this.settings.hide_name_column &&
			this.meta.title_field &&
			this.meta.title_field !== "name"
		) {
			this.columns.push({
				type: "Field",
				df: {
					label: __("ID"),
					fieldname: "name",
				},
			});
		}
	}

	reorder_listview_fields() {
		let fields_order = [];
		let fields = JSON.parse(this.list_view_settings.fields);

		//title and tags field is fixed
		fields_order.push(this.columns[0]);
		fields_order.push(this.columns[1]);
		this.columns.splice(0, 2);

		for (let fld in fields) {
			for (let col in this.columns) {
				let field = fields[fld];
				let column = this.columns[col];

				if (column.type == "Status" && field.fieldname == "status_field") {
					fields_order.push(column);
					break;
				} else if (column.type == "Field" && field.fieldname === column.df.fieldname) {
					fields_order.push(column);
					break;
				}
			}
		}

		return fields_order;
	}

	get_documentation_link() {
		if (this.meta.documentation) {
			return `<a href="${this.meta.documentation}" target="blank" class="meta-description small text-muted">Need Help?</a>`;
		}
		return "";
	}

	get_no_result_message() {
		let help_link = this.get_documentation_link();
		let filters = this.filter_area && this.filter_area.get();

		let has_filters_set = filters && filters.length;
		let no_result_message = has_filters_set
			? __("No {0} found with matching filters. Clear filters to see all {0}.", [
					__(this.doctype),
			  ])
			: this.meta.description
			? __(this.meta.description)
			: __("You haven't created a {0} yet", [__(this.doctype)]);

		let new_button_label = has_filters_set
			? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view")
			: __(
					"Create your first {0}",
					[__(this.doctype)],
					"Create a new document from list view"
			  );
		let empty_state_image =
			this.settings.empty_state_image ||
			"/assets/frappe/images/ui-states/list-empty-state.svg";

		const new_button = this.can_create
			? `<p><button class="btn btn-default btn-sm btn-new-doc hidden-xs">
				${new_button_label}
			</button> <button class="btn btn-primary btn-new-doc visible-xs">
				${__("Create New", null, "Create a new document from list view")}
			</button></p>`
			: "";

		return `<div class="msg-box no-border">
			<div>
				<img src="${empty_state_image}" alt="Generic Empty State" class="null-state">
			</div>
			<p>${no_result_message}</p>
			${new_button}
			${help_link}
		</div>`;
	}

	freeze() {
		if (this.list_view_settings && !this.list_view_settings.disable_count) {
			this.get_count_element().html(
				`<span>${__("Refreshing", null, "Document count in list view")}...</span>`
			);
		}
	}

	get_args() {
		const args = super.get_args();

		if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
			args.with_comment_count = 1;
		} else {
			args.with_comment_count = 0;
		}

		return args;
	}

	before_refresh() {
		if (frappe.route_options && this.filter_area) {
			this.filters = this.parse_filters_from_route_options();
			frappe.route_options = null;

			if (this.filters.length > 0) {
				return this.filter_area
					.clear(false)
					.then(() => this.filter_area.set(this.filters));
			}
		}

		return Promise.resolve();
	}

	parse_filters_from_settings() {
		return (this.settings.filters || []).map((f) => {
			if (f.length === 3) {
				f = [this.doctype, f[0], f[1], f[2]];
			}
			return f;
		});
	}

	toggle_result_area() {
		super.toggle_result_area();
		this.toggle_actions_menu_button(this.$result.find(".list-row-check:checked").length > 0);
	}

	toggle_actions_menu_button(toggle) {
		if (toggle) {
			this.page.show_actions_menu();
			this.page.clear_primary_action();
		} else {
			this.page.hide_actions_menu();
			this.set_primary_action();
		}
	}

	render_header(refresh_header = false) {
		if (refresh_header) {
			this.$result.find(".list-row-head").remove();
		}
		if (this.$result.find(".list-row-head").length === 0) {
			// append header once
			this.$result.prepend(this.get_header_html());
		}
	}

	render_skeleton() {
		const $row = this.get_list_row_html_skeleton(
			'<div><input type="checkbox" class="render-list-checkbox"/></div>'
		);
		this.$result.append($row);
	}

	before_render() {
		this.settings.before_render && this.settings.before_render();
		frappe.model.user_settings.save(this.doctype, "last_view", this.view_name);
		this.save_view_user_settings({
			filters: this.filter_area && this.filter_area.get(),
			sort_by: this.sort_selector && this.sort_selector.sort_by,
			sort_order: this.sort_selector && this.sort_selector.sort_order,
		});
		this.toggle_paging && this.$paging_area.toggle(false);
	}

	after_render() {
		this.$no_result.html(`
			<div class="no-result text-muted flex justify-center align-center">
				${this.get_no_result_message()}
			</div>
		`);
		this.setup_new_doc_event();
		this.toggle_paging && this.$paging_area.toggle(true);
	}

	render() {
		this.render_list();
		this.set_rows_as_checked();
		this.render_count();
	}

	render_list() {
		// clear rows
		this.$result.find(".list-row-container").remove();

		if (this.data.length > 0) {
			// append rows
			let idx = 0;
			for (let doc of this.data) {
				doc._idx = idx++;
				this.$result.append(this.get_list_row_html(doc));
			}
		}
	}

	render_count() {
		if (this.list_view_settings.disable_count) return;

		let me = this;
		let $count = this.get_count_element();
		this.get_count_str().then((count) => {
			$count.html(`<span>${count}</span>`);
			if (this.count_upper_bound && this.count_upper_bound == this.total_count) {
				$count.attr(
					"title",
					__(
						"The count shown is an estimated count. Click here to see the accurate count."
					)
				);
				$count.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" });
				$count.on("click", () => {
					me.count_upper_bound = 0;
					$count.off("click");
					$count.tooltip("disable");
					me.freeze();
					me.render_count();
				});
			}
		});
	}

	get_count_element() {
		return this.$result.find(".list-count");
	}

	get_header_html() {
		if (!this.columns) {
			return;
		}

		const subject_field = this.columns[0].df;
		let subject_html = `
			<input class="level-item list-check-all" type="checkbox"
				title="${__("Select All")}">
			<span class="level-item" data-sort-by="${subject_field.fieldname}"
				title="${__("Click to sort by {0}", [subject_field.label])}">
				${__(subject_field.label)}
			</span>
		`;
		const $columns = this.columns
			.map((col) => {
				let classes = [
					"list-row-col ellipsis",
					col.type == "Subject" ? "list-subject level" : "hidden-xs",
					col.type == "Tag" ? "tag-col hide" : "",
					frappe.model.is_numeric_field(col.df) ? "text-right" : "",
				].join(" ");

				let html = "";
				if (col.type === "Subject") {
					html = subject_html;
				} else {
					const fieldname = col.df?.fieldname;
					const label = __(col.df?.label || col.type, null, col.df?.parent);
					const title = __("Click to sort by {0}", [label]);
					const attrs = fieldname ? `data-sort-by="${fieldname}" title="${title}"` : "";
					html = `<span ${attrs}>${label}</span>`;
				}

				return `<div class="${classes}">${html}</div>
			`;
			})
			.join("");

		const right_html = `
			<span class="list-count"></span>
			<span class="level-item list-liked-by-me hidden-xs">
				<span title="${__("Liked by me")}">
					${frappe.utils.icon("es-solid-heart", "sm", "like-icon")}
				</span>
			</span>
		`;

		return this.get_header_html_skeleton($columns, right_html);
	}

	get_header_html_skeleton(left = "", right = "") {
		return `
			<header class="level list-row-head text-muted">
				<div class="level-left list-header-subject">
					${left}
				</div>
				<div class="level-left checkbox-actions">
					<div class="level list-subject">
						<input class="level-item list-check-all" type="checkbox"
							title="${__("Select All")}">
						<span class="level-item list-header-meta"></span>
					</div>
				</div>
				<div class="level-right">
					${right}
				</div>
			</header>
		`;
	}

	get_left_html(doc) {
		return this.columns.map((col) => this.get_column_html(col, doc)).join("");
	}

	get_right_html(doc) {
		return this.get_meta_html(doc);
	}

	get_list_row_html(doc) {
		return this.get_list_row_html_skeleton(this.get_left_html(doc), this.get_right_html(doc));
	}

	get_list_row_html_skeleton(left = "", right = "") {
		return `
			<div class="list-row-container" tabindex="1">
				<div class="level list-row">
					<div class="level-left ellipsis">
						${left}
					</div>
					<div class="level-right text-muted ellipsis">
						${right}
					</div>
				</div>
				<div class="list-row-border"></div>
			</div>
		`;
	}

	get_column_html(col, doc) {
		if (col.type === "Status" || col.df?.options == "Workflow State") {
			let show_workflow_state = col.df?.options == "Workflow State";
			return `
				<div class="list-row-col hidden-xs ellipsis">
					${this.get_indicator_html(doc, show_workflow_state)}
				</div>
			`;
		}

		if (col.type === "Tag") {
			const tags_display_class = !this.tags_shown ? "hide" : "";
			let tags_html = doc._user_tags
				? this.get_tags_html(doc._user_tags, 2, true)
				: '<div class="tags-empty">-</div>';
			return `
				<div class="list-row-col tag-col ${tags_display_class} hidden-xs ellipsis">
					${tags_html}
				</div>
			`;
		}

		const df = col.df || {};
		const label = df.label;
		const fieldname = df.fieldname;
		const link_title_fieldname = this.link_field_title_fields[fieldname];
		const value = doc[fieldname] || "";
		let value_display = link_title_fieldname
			? doc[fieldname + "_" + link_title_fieldname] || value
			: value;

		let translated_doctypes = frappe.boot?.translated_doctypes || [];
		if (translated_doctypes.includes(df.options)) {
			value_display = __(value_display);
		}

		const format = () => {
			if (df.fieldtype === "Code") {
				return value;
			} else if (df.fieldtype === "Percent") {
				return `<div class="progress" style="margin: 0px;">
						<div class="progress-bar progress-bar-success" role="progressbar"
							aria-valuenow="${value}"
							aria-valuemin="0" aria-valuemax="100" style="width: ${Math.round(value)}%;">
						</div>
					</div>`;
			} else {
				return frappe.format(value, df, null, doc);
			}
		};

		const field_html = () => {
			let html;
			let _value;
			let strip_html_required =
				df.fieldtype == "Text Editor" ||
				(df.fetch_from && ["Text", "Small Text"].includes(df.fieldtype));

			if (strip_html_required) {
				_value = strip_html(value_display);
			} else {
				_value =
					typeof value_display === "string"
						? frappe.utils.escape_html(value_display)
						: value_display;
			}

			if (df.fieldtype === "Rating") {
				let out_of_ratings = df.options || 5;
				_value = _value * out_of_ratings;
			}

			if (df.fieldtype === "Image") {
				html = df.options
					? `<img src="${doc[df.options]}"
					style="max-height: 30px; max-width: 100%;">`
					: `<div class="missing-image small">
						${frappe.utils.icon("restriction")}
					</div>`;
			} else if (df.fieldtype === "Select") {
				html = `<span class="filterable indicator-pill ${frappe.utils.guess_colour(
					_value
				)} ellipsis"
					data-filter="${fieldname},=,${value}">
					<span class="ellipsis"> ${__(_value)} </span>
				</span>`;
			} else if (df.fieldtype === "Link") {
				html = `<a class="filterable ellipsis"
					data-filter="${fieldname},=,${value}">
					${_value}
				</a>`;
			} else if (
				["Text Editor", "Text", "Small Text", "HTML Editor", "Markdown Editor"].includes(
					df.fieldtype
				)
			) {
				html = `<span class="ellipsis">
					${_value}
				</span>`;
			} else {
				html = `<a class="filterable ellipsis"
					data-filter="${fieldname},=,${frappe.utils.escape_html(value)}">
					${format()}
				</a>`;
			}

			return `<span class="ellipsis"
				title="${__(label)}: ${frappe.utils.escape_html(_value)}">
				${html}
			</span>`;
		};

		const class_map = {
			Subject: "list-subject level",
			Field: "hidden-xs",
		};
		const css_class = [
			"list-row-col ellipsis",
			class_map[col.type],
			frappe.model.is_numeric_field(df) ? "text-right" : "",
		].join(" ");

		let column_html;
		if (
			this.settings.formatters &&
			this.settings.formatters[fieldname] &&
			col.type !== "Subject"
		) {
			column_html = this.settings.formatters[fieldname](value, df, doc);
		} else {
			column_html = {
				Subject: this.get_subject_element(doc, value_display).innerHTML,
				Field: field_html(),
			}[col.type];
		}

		return `
			<div class="${css_class}">
				${column_html}
			</div>
		`;
	}

	get_tags_html(user_tags, limit, colored = false) {
		let get_tag_html = (tag) => {
			let color = "",
				style = "";
			if (tag) {
				if (colored) {
					color = frappe.get_palette(tag);
					style = `background-color: var(${color[0]}); color: var(${color[1]})`;
				}

				return `<div class="tag-pill ellipsis" title="${tag}" style="${style}">${tag}</div>`;
			}
		};
		return user_tags
			.split(",")
			.slice(1, limit + 1)
			.map(get_tag_html)
			.join("");
	}

	get_meta_html(doc) {
		let html = "";

		let settings_button = null;
		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 = ``;

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

		let comment_count = null;
		if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
			comment_count = `<span class="comment-count d-flex align-items-center">
				${frappe.utils.icon("es-line-chat-alt")}
				${doc._comment_count > 99 ? "99+" : doc._comment_count || 0}
			</span>`;
		}

		html += `
			<div class="level-item list-row-activity hidden-xs">
				<div class="hidden-md hidden-xs">
					${settings_button || assigned_to}
				</div>
				<span class="modified">${modified}</span>
				${comment_count || ""}
				${comment_count ? '<span class="mx-2">·</span>' : ""}
				<span class="list-row-like hidden-xs style="margin-bottom: 1px;">
					${this.get_like_html(doc)}
				</span>
			</div>
			<div class="level-item visible-xs text-right">
				${this.get_indicator_html(doc)}
			</div>
		`;

		return html;
	}

	get_count_str() {
		let current_count = this.data.length;
		let count_without_children = this.data.uniqBy((d) => d.name).length;

		return frappe.db
			.count(this.doctype, {
				filters: this.get_filters_for_args(),
				limit: this.count_upper_bound,
			})
			.then((total_count) => {
				this.total_count = total_count || current_count;
				this.count_without_children =
					count_without_children !== current_count ? count_without_children : undefined;

				let count_str;
				if (this.total_count === this.count_upper_bound) {
					count_str = `${format_number(this.total_count - 1, null, 0)}+`;
				} else {
					count_str = format_number(this.total_count, null, 0);
				}

				let str = __("{0} of {1}", [format_number(current_count, null, 0), count_str]);
				if (this.count_without_children) {
					str = __("{0} of {1} ({2} rows with children)", [
						count_without_children,
						count_str,
						current_count,
					]);
				}
				return str;
			});
	}

	get_form_link(doc) {
		if (this.settings.get_form_link) {
			return this.settings.get_form_link(doc);
		}

		return `/app/${encodeURIComponent(
			frappe.router.slug(frappe.router.doctype_layout || this.doctype)
		)}/${encodeURIComponent(cstr(doc.name))}`;
	}

	get_seen_class(doc) {
		const seen_by = doc._seen ? JSON.parse(doc._seen) : [];
		return seen_by.includes(frappe.session.user) ? "" : "bold";
	}

	get_like_html(doc) {
		const liked_by = doc._liked_by ? JSON.parse(doc._liked_by) : [];
		const is_liked = liked_by.includes(frappe.session.user);
		const title = liked_by.map((u) => frappe.user_info(u).fullname).join(", ");

		const div = document.createElement("div");
		div.appendChild(
			this._element_factory.get_like_element(doc.name, is_liked, liked_by, title)
		);

		return div.innerHTML;
	}

	get_subject_element(doc, title) {
		const ef = this._element_factory;
		const div = document.createElement("div");
		const checkboxspan = ef.get_checkboxspan_element();

		const ellipsisSpan = document.createElement("span");
		const seen = this.get_seen_class(doc);
		if (seen) {
			ellipsisSpan.classList.add("level-item", seen, "ellipsis");
		}

		div.appendChild(checkboxspan).appendChild(ef.get_checkbox_element(doc.name));
		div.appendChild(ellipsisSpan).appendChild(
			ef.get_link_element(
				doc.name,
				this.get_form_link(doc),
				this.get_subject_text(doc, title)
			)
		);

		return div;
	}

	get_subject_text(doc, title) {
		const subject_field = this.columns[0].df;
		let value = title || doc[subject_field.fieldname];
		if (this.settings.formatters && this.settings.formatters[subject_field.fieldname]) {
			let formatter = this.settings.formatters[subject_field.fieldname];
			value = formatter(value, subject_field, doc);
		}

		if (!value) {
			value = doc.name;
		}

		if (frappe.model.html_fieldtypes.includes(subject_field.fieldtype)) {
			// NOTE: this is very slow, so only do it for HTML fields
			return frappe.utils.html2text(value);
		} else {
			return value;
		}
	}

	get_indicator_html(doc, show_workflow_state) {
		const indicator = frappe.get_indicator(doc, this.doctype, show_workflow_state);
		// sequence is important
		const docstatus_description = [
			__("Document is in draft state"),
			__("Document has been submitted"),
			__("Document has been cancelled"),
		];
		const title = docstatus_description[doc.docstatus || 0];
		if (indicator) {
			return `<span class="indicator-pill ${
				indicator[1]
			} filterable no-indicator-dot ellipsis"
				data-filter='${indicator[2]}' title='${title}'>
				<span class="ellipsis"> ${__(indicator[0])}</span>
			</span>`;
		}
		return "";
	}

	get_indicator_dot(doc) {
		const indicator = frappe.get_indicator(doc, this.doctype);
		if (!indicator) return "";
		return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`;
	}

	get_image_url(doc) {
		let url = doc.image ? doc.image : doc[this.meta.image_field];
		// absolute url for mobile
		if (window.cordova && !frappe.utils.is_url(url)) {
			url = frappe.base_url + url;
		}
		return url || null;
	}

	setup_events() {
		this.setup_filterable();
		this.setup_sort_by();
		this.setup_list_click();
		this.setup_drag_click();
		this.setup_tag_event();
		this.setup_new_doc_event();
		this.setup_check_events();
		this.setup_like();
		this.setup_realtime_updates();
		this.setup_action_handler();
		this.setup_keyboard_navigation();
	}

	setup_keyboard_navigation() {
		let focus_first_row = () => {
			this.$result.find(".list-row-container:first").focus();
		};
		let focus_next = () => {
			$(document.activeElement).next().focus();
		};
		let focus_prev = () => {
			$(document.activeElement).prev().focus();
		};
		let list_row_focused = () => {
			return $(document.activeElement).is(".list-row-container");
		};
		let check_row = ($row) => {
			let $input = $row.find("input[type=checkbox]");
			$input.click();
		};
		let get_list_row_if_focused = () =>
			list_row_focused() ? $(document.activeElement) : null;

		let is_current_page = () => this.page.wrapper.is(":visible");
		let is_input_focused = () => $(document.activeElement).is("input");

		let handle_navigation = (direction) => {
			if (!is_current_page() || is_input_focused()) return false;

			let $list_row = get_list_row_if_focused();
			if ($list_row) {
				direction === "down" ? focus_next() : focus_prev();
			} else {
				focus_first_row();
			}
		};

		frappe.ui.keys.add_shortcut({
			shortcut: "down",
			action: () => handle_navigation("down"),
			description: __("Navigate list down", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "up",
			action: () => handle_navigation("up"),
			description: __("Navigate list up", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "shift+down",
			action: () => {
				if (!is_current_page() || is_input_focused()) return false;
				let $list_row = get_list_row_if_focused();
				check_row($list_row);
				focus_next();
			},
			description: __(
				"Select multiple list items",
				null,
				"Description of a list view shortcut"
			),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "shift+up",
			action: () => {
				if (!is_current_page() || is_input_focused()) return false;
				let $list_row = get_list_row_if_focused();
				check_row($list_row);
				focus_prev();
			},
			description: __(
				"Select multiple list items",
				null,
				"Description of a list view shortcut"
			),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "enter",
			action: () => {
				let $list_row = get_list_row_if_focused();
				if ($list_row) {
					$list_row.find("a[data-name]")[0].click();
					return true;
				}
				return false;
			},
			description: __("Open list item", null, "Description of a list view shortcut"),
			page: this.page,
		});

		frappe.ui.keys.add_shortcut({
			shortcut: "space",
			action: () => {
				let $list_row = get_list_row_if_focused();
				if ($list_row) {
					check_row($list_row);
					return true;
				}
				return false;
			},
			description: __("Select list item", null, "Description of a list view shortcut"),
			page: this.page,
		});
	}

	setup_filterable() {
		// filterable events
		this.$result.on("click", ".filterable", (e) => {
			if (e.metaKey || e.ctrlKey) return;
			e.stopPropagation();
			const $this = $(e.currentTarget);
			const filters = $this.attr("data-filter").split("|");
			const filters_to_apply = filters.map((f) => {
				f = f.split(",");
				if (f[2] === "Today") {
					f[2] = frappe.datetime.get_today();
				} else if (f[2] == "User") {
					f[2] = frappe.session.user;
				}
				this.filter_area.remove(f[0]);
				return [this.doctype, f[0], f[1], f.slice(2).join(",")];
			});
			this.filter_area.add(filters_to_apply);
		});
	}

	setup_sort_by() {
		this.$result.on("click", "[data-sort-by]", (e) => {
			const sort_by = e.currentTarget.getAttribute("data-sort-by");
			if (!sort_by) return;
			let sort_order = "asc"; // always start with asc
			if (this.sort_by === sort_by) {
				// unless it's the same field, then toggle
				sort_order = this.sort_order === "asc" ? "desc" : "asc";
			}
			this.sort_selector.set_value(sort_by, sort_order);
			this.on_sort_change(sort_by, sort_order);
		});
	}

	setup_list_click() {
		this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
			const $target = $(e.target);
			// tick checkbox if Ctrl/Meta key is pressed
			if ((e.ctrlKey || e.metaKey) && !$target.is("a")) {
				const $list_row = $(e.currentTarget);
				const $check = $list_row.find(".list-row-checkbox");
				$check.prop("checked", !$check.prop("checked"));
				e.preventDefault();
				this.on_row_checked();
				return;
			}
			// don't open form when checkbox, like, filterable are clicked
			if (
				$target.hasClass("filterable") ||
				$target.hasClass("select-like") ||
				$target.hasClass("file-select") ||
				$target.hasClass("list-row-like") ||
				$target.is(":checkbox")
			) {
				e.stopPropagation();
				return;
			}

			// link, let the event be handled via set_route
			if ($target.is("a")) return;

			// clicked on the row, open form
			const $row = $(e.currentTarget);
			const link = $row.find(".list-subject a").get(0);
			if (link) {
				frappe.set_route(link.pathname);
				return false;
			}
		});
	}

	setup_drag_click() {
		/*
			Click on the check box in the list view and
			drag through the rows to select.

			Do it again to unselect.

			If the first click is on checked checkbox, then it will unselect rows on drag,
			else if it is unchecked checkbox, it will select rows on drag.
		*/
		this.dragClick = false;
		this.$result.on("mousedown", ".list-row-checkbox", (e) => {
			e.stopPropagation?.();
			e.preventDefault?.();
			this.dragClick = true;
			this.check = !e.target.checked;
		});
		$(document).on("mouseup", () => {
			this.dragClick = false;
		});
		this.$result.on("mousemove", ".level.list-row", (e) => {
			if (this.dragClick) {
				this.check_row_on_drag(e, this.check);
			}
		});
	}

	check_row_on_drag(event, check = true) {
		$(event.target).find(".list-row-checkbox").prop("checked", check);
		this.on_row_checked();
	}

	setup_action_handler() {
		this.$result.on("click", ".btn-action", (e) => {
			const $button = $(e.currentTarget);
			const doc = this.data[$button.attr("data-idx")];
			this.settings.button.action(doc);
			e.stopPropagation();
			return false;
		});
	}

	setup_check_events() {
		this.$result.on("change", "input[type=checkbox]", (e) => {
			const $target = $(e.currentTarget);

			if ($target.is(".list-header-subject .list-check-all")) {
				const $check = this.$result.find(".checkbox-actions .list-check-all");
				$check.prop("checked", $target.prop("checked"));
				$check.trigger("change");
			} else if ($target.is(".checkbox-actions .list-check-all")) {
				const $check = this.$result.find(".list-header-subject .list-check-all");
				$check.prop("checked", $target.prop("checked"));

				this.$result.find(".list-row-checkbox").prop("checked", $target.prop("checked"));
			} else if ($target.attr("data-parent")) {
				this.$result
					.find(`.${$target.attr("data-parent")}`)
					.find(".list-row-checkbox")
					.prop("checked", $target.prop("checked"));
			}

			this.on_row_checked();
		});

		this.$result.on("click", ".list-row-checkbox", (e) => {
			const $target = $(e.currentTarget);

			// shift select checkboxes
			if (e.shiftKey && this.$checkbox_cursor && !$target.is(this.$checkbox_cursor)) {
				const name_1 = decodeURIComponent(this.$checkbox_cursor.data().name);
				const name_2 = decodeURIComponent($target.data().name);
				const index_1 = this.data.findIndex((d) => d.name === name_1);
				const index_2 = this.data.findIndex((d) => d.name === name_2);
				let [min_index, max_index] = [index_1, index_2];

				if (min_index > max_index) {
					[min_index, max_index] = [max_index, min_index];
				}

				let docnames = this.data.slice(min_index + 1, max_index).map((d) => d.name);
				const selector = docnames
					.map((name) => `.list-row-checkbox[data-name="${encodeURIComponent(name)}"]`)
					.join(",");
				this.$result.find(selector).prop("checked", true);
			}

			this.$checkbox_cursor = $target;

			this.update_checkbox($target);
		});

		let me = this;
		this.page.actions_btn_group.on("show.bs.dropdown", () => {
			me.toggle_workflow_actions();
		});
	}

	setup_like() {
		this.$result.on("click", ".like-action", (e) => {
			const $this = $(e.currentTarget);
			const { doctype, name } = $this.data();
			frappe.ui.toggle_like($this, doctype, name);

			return false;
		});

		this.$result.on("click", ".list-liked-by-me", (e) => {
			const $this = $(e.currentTarget);
			$this.toggleClass("active");

			if ($this.hasClass("active")) {
				this.filter_area.add(
					this.doctype,
					"_liked_by",
					"like",
					"%" + frappe.session.user + "%"
				);
			} else {
				this.filter_area.remove("_liked_by");
			}
		});
	}

	setup_new_doc_event() {
		this.$no_result.find(".btn-new-doc").click(() => {
			if (this.settings.primary_action) {
				this.settings.primary_action();
			} else {
				this.make_new_doc();
			}
		});
	}

	setup_tag_event() {
		this.tags_shown = false;
		this.list_sidebar &&
			this.list_sidebar.parent.on("click", ".list-tag-preview", () => {
				this.tags_shown = !this.tags_shown;
				this.toggle_tags();
			});
	}

	setup_realtime_updates() {
		this.pending_document_refreshes = [];

		if (this.list_view_settings?.disable_auto_refresh || this.realtime_events_setup) {
			return;
		}
		frappe.realtime.doctype_subscribe(this.doctype);
		frappe.realtime.off("list_update");
		frappe.realtime.on("list_update", (data) => {
			if (data?.doctype !== this.doctype) {
				return;
			}

			// if some bulk operation is happening by selecting list items, don't refresh
			if (this.$checks && this.$checks.length) {
				return;
			}

			if (this.avoid_realtime_update()) {
				return;
			}

			this.pending_document_refreshes.push(data);
			this.debounced_refresh();
		});
		this.realtime_events_setup = true;
	}

	disable_realtime_updates() {
		frappe.realtime.doctype_unsubscribe(this.doctype);
		this.realtime_events_setup = false;
	}

	process_document_refreshes() {
		if (!this.pending_document_refreshes.length) return;

		const route = frappe.get_route() || [];
		if (!cur_list || route[0] != "List" || cur_list.doctype != route[1]) {
			// wait till user is back on list view before refreshing
			this.pending_document_refreshes = [];
			this.disable_realtime_updates();
			return;
		}

		const names = this.pending_document_refreshes.map((d) => d.name);
		this.pending_document_refreshes = this.pending_document_refreshes.filter(
			(d) => names.indexOf(d.name) === -1
		);

		if (!names.length) return;

		// filters to get only the doc with this name
		const call_args = this.get_call_args();
		call_args.args.filters.push([this.doctype, "name", "in", names]);
		call_args.args.start = 0;

		frappe.call(call_args).then(({ message }) => {
			if (!message) return;
			const data = frappe.utils.dict(message.keys, message.values);

			if (!(data && data.length)) {
				// this doc was changed and should not be visible
				// in the listview according to filters applied
				// let's remove it manually
				this.data = this.data.filter((d) => !names.includes(d.name));
				for (let name of names) {
					this.$result
						.find(`.list-row-checkbox[data-name='${name.replace(/'/g, "\\'")}']`)
						.closest(".list-row-container")
						.remove();
				}
				return;
			}

			data.forEach((datum) => {
				const index = this.data.findIndex((doc) => doc.name === datum.name);

				if (index === -1) {
					// append new data
					this.data.push(datum);
				} else {
					// update this data in place
					this.data[index] = datum;
				}
			});

			this.data.sort((a, b) => {
				const a_value = a[this.sort_by] || "";
				const b_value = b[this.sort_by] || "";

				let return_value = 0;
				if (a_value > b_value) {
					return_value = 1;
				}

				if (b_value > a_value) {
					return_value = -1;
				}

				if (this.sort_order === "desc") {
					return_value = -return_value;
				}
				return return_value;
			});
			if (this.$checks && this.$checks.length) {
				this.set_rows_as_checked();
			}
			this.toggle_result_area();
			this.render_list();
		});
	}

	avoid_realtime_update() {
		if (this.filter_area?.is_being_edited()) {
			return true;
		}
		// this is set when a bulk operation is called from a list view which might update the list view
		// this is to avoid the list view from refreshing a lot of times
		// the list view is updated once after the bulk operation is complete
		if (this.disable_list_update) {
			return true;
		}
		return false;
	}

	set_rows_as_checked() {
		if (!this.$checks || !this.$checks.length) {
			return;
		}

		$.each(this.$checks, (i, el) => {
			let docname = $(el).attr("data-name");
			this.$result.find(`.list-row-checkbox[data-name='${docname}']`).prop("checked", true);
		});
		this.on_row_checked();
	}

	on_row_checked() {
		this.$list_head_subject =
			this.$list_head_subject || this.$result.find("header .list-header-subject");
		this.$checkbox_actions =
			this.$checkbox_actions || this.$result.find("header .checkbox-actions");

		this.$checks = this.$result.find(".list-row-checkbox:checked");

		this.$list_head_subject.toggle(this.$checks.length === 0);
		this.$checkbox_actions.toggle(this.$checks.length > 0);

		if (this.$checks.length === 0) {
			this.$list_head_subject.find(".list-check-all").prop("checked", false);
		} else {
			this.$checkbox_actions
				.find(".list-header-meta")
				.html(__("{0} items selected", [this.$checks.length]));
			this.$checkbox_actions.show();
			this.$list_head_subject.hide();
		}
		this.update_checkbox();
		this.toggle_actions_menu_button(this.$checks.length > 0);
	}

	toggle_tags() {
		this.$result.find(".tag-col").toggleClass("hide");
		const preview_label = this.tags_shown ? __("Hide Tags") : __("Show Tags");
		this.list_sidebar.parent.find(".list-tag-preview").text(preview_label);
	}

	get_checked_items(only_docnames) {
		const docnames = Array.from(this.$checks || []).map((check) =>
			cstr(unescape($(check).data().name))
		);

		if (only_docnames) return docnames;

		return this.data.filter((d) => docnames.includes(d.name));
	}

	clear_checked_items() {
		this.$checks && this.$checks.prop("checked", false);
		this.on_row_checked();
	}

	save_view_user_settings(obj) {
		return frappe.model.user_settings.save(this.doctype, this.view_name, obj);
	}

	on_update() {}

	update_url_with_filters() {
		if (frappe.get_route_str() == this.page_name && !this.report_name) {
			// only update URL if the route still matches current page.
			// do not update if current list is a "saved report".
			window.history.replaceState(null, null, this.get_url_with_filters());
		}
	}

	get_url_with_filters() {
		let search_params = this.get_search_params();

		let full_url = window.location.href.replace(window.location.search, "");
		if (search_params.size) {
			full_url += "?" + search_params.toString();
		}
		return full_url;
	}

	get_search_params() {
		let search_params = new URLSearchParams();

		this.get_filters_for_args().forEach((filter) => {
			if (filter[2] === "=") {
				search_params.append(filter[1], filter[3]);
			} else {
				search_params.append(filter[1], JSON.stringify([filter[2], filter[3]]));
			}
		});
		return search_params;
	}

	get_menu_items() {
		const doctype = this.doctype;
		const items = [];

		if (frappe.model.can_import(doctype, null, this.meta)) {
			items.push({
				label: __("Import", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("list", "data-import", {
						reference_doctype: doctype,
					}),
				standard: true,
			});
		}

		if (frappe.user_roles.includes("System Manager")) {
			items.push({
				label: __("User Permissions", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("list", "user-permission", {
						allow: doctype,
					}),
				standard: true,
			});
		}

		if (frappe.user_roles.includes("System Manager")) {
			items.push({
				label: __("Role Permissions Manager", null, "Button in list view menu"),
				action: () =>
					frappe.set_route("permission-manager", {
						doctype,
					}),
				standard: true,
			});
		}

		if (
			frappe.model.can_create("Custom Field") &&
			frappe.model.can_create("Property Setter")
		) {
			items.push({
				label: __("Customize", null, "Button in list view menu"),
				action: () => {
					if (!this.meta) return;
					if (this.meta.custom) {
						frappe.set_route("form", "doctype", doctype);
					} else if (!this.meta.custom) {
						frappe.set_route("form", "customize-form", {
							doc_type: doctype,
						});
					}
				},
				standard: true,
				shortcut: "Ctrl+J",
			});
		}

		items.push({
			label: __("Toggle Sidebar", null, "Button in list view menu"),
			action: () => this.toggle_side_bar(),
			condition: () => !this.hide_sidebar,
			standard: true,
			shortcut: "Ctrl+K",
		});

		if (frappe.user.has_role("System Manager") && frappe.boot.developer_mode === 1) {
			// edit doctype
			items.push({
				label: __("Edit DocType", null, "Button in list view menu"),
				action: () => frappe.set_route("form", "doctype", doctype),
				standard: true,
			});
		}

		if (frappe.user.has_role("System Manager")) {
			if (this.get_view_settings) {
				items.push(this.get_view_settings());
			}
		}

		return items;
	}

	get_view_settings() {
		return {
			label: __("List Settings", null, "Button in list view menu"),
			action: () => this.show_list_settings(),
			standard: true,
		};
	}

	show_list_settings() {
		frappe.model.with_doctype(this.doctype, () => {
			new ListSettings({
				listview: this,
				doctype: this.doctype,
				settings: this.list_view_settings,
				meta: frappe.get_meta(this.doctype),
			});
		});
	}

	get_workflow_action_menu_items() {
		const workflow_actions = [];
		const me = this;

		if (frappe.model.has_workflow(this.doctype)) {
			const actions = frappe.workflow.get_all_transition_actions(this.doctype);
			actions.forEach((action) => {
				workflow_actions.push({
					label: __(action),
					name: action,
					action: () => {
						me.disable_list_update = true;
						frappe
							.xcall("frappe.model.workflow.bulk_workflow_approval", {
								docnames: this.get_checked_items(true),
								doctype: this.doctype,
								action: action,
							})
							.finally(() => {
								me.disable_list_update = false;
							});
					},
					is_workflow_action: true,
				});
			});
		}
		return workflow_actions;
	}

	toggle_workflow_actions() {
		if (!frappe.model.has_workflow(this.doctype)) return;

		Object.keys(this.workflow_action_items).forEach((key) => {
			this.workflow_action_items[key].addClass("disabled");
		});
		const checked_items = this.get_checked_items();

		frappe
			.xcall("frappe.model.workflow.get_common_transition_actions", {
				docs: checked_items,
				doctype: this.doctype,
			})
			.then((actions) => {
				Object.keys(this.workflow_action_items).forEach((key) => {
					this.workflow_action_items[key].removeClass("disabled");
					this.workflow_action_items[key].toggle(actions.includes(key));
				});
			});
	}

	get_actions_menu_items() {
		const doctype = this.doctype;
		const actions_menu_items = [];
		const bulk_operations = new BulkOperations({ doctype: this.doctype });

		const is_field_editable = (field_doc) => {
			return (
				field_doc.fieldname &&
				frappe.model.is_value_type(field_doc) &&
				field_doc.fieldtype !== "Read Only" &&
				!field_doc.hidden &&
				!field_doc.read_only &&
				!field_doc.is_virtual
			);
		};

		const has_editable_fields = (doctype) => {
			return frappe.meta
				.get_docfields(doctype)
				.some((field_doc) => is_field_editable(field_doc));
		};

		const has_submit_permission = (doctype) => {
			return frappe.perm.has_perm(doctype, 0, "submit");
		};

		// utility
		const bulk_assignment = () => {
			return {
				label: __("Assign To", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.assign(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_assignment_rule = () => {
			return {
				label: __("Apply Assignment Rule", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.apply_assignment_rule(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_add_tags = () => {
			return {
				label: __("Add Tags", null, "Button in list view actions menu"),
				action: () => {
					this.disable_list_update = true;
					bulk_operations.add_tags(this.get_checked_items(true), () => {
						this.disable_list_update = false;
						this.clear_checked_items();
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_printing = () => {
			return {
				label: __("Print", null, "Button in list view actions menu"),
				action: () => bulk_operations.print(this.get_checked_items()),
				standard: true,
			};
		};

		const bulk_delete = () => {
			return {
				label: __("Delete", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true).map((docname) =>
						docname.toString()
					);
					let message = __(
						"Delete {0} item permanently?",
						[docnames.length],
						"Title of confirmation dialog"
					);
					if (docnames.length > 1) {
						message = __(
							"Delete {0} items permanently?",
							[docnames.length],
							"Title of confirmation dialog"
						);
					}
					frappe.confirm(message, () => {
						this.disable_list_update = true;
						bulk_operations.delete(docnames, () => {
							this.disable_list_update = false;
							this.clear_checked_items();
							this.refresh();
						});
					});
				},
				standard: true,
			};
		};

		const bulk_cancel = () => {
			return {
				label: __("Cancel", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);
					if (docnames.length > 0) {
						frappe.confirm(
							__(
								"Cancel {0} documents?",
								[docnames.length],
								"Title of confirmation dialog"
							),
							() => {
								this.disable_list_update = true;
								bulk_operations.submit_or_cancel(docnames, "cancel", () => {
									this.disable_list_update = false;
									this.clear_checked_items();
									this.refresh();
								});
							}
						);
					}
				},
				standard: true,
			};
		};

		const bulk_submit = () => {
			return {
				label: __("Submit", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);
					if (docnames.length > 0) {
						frappe.confirm(
							__(
								"Submit {0} documents?",
								[docnames.length],
								"Title of confirmation dialog"
							),
							() => {
								this.disable_list_update = true;
								bulk_operations.submit_or_cancel(docnames, "submit", () => {
									this.disable_list_update = false;
									this.clear_checked_items();
									this.refresh();
								});
							}
						);
					}
				},
				standard: true,
			};
		};

		const bulk_edit = () => {
			return {
				label: __("Edit", null, "Button in list view actions menu"),
				action: () => {
					let field_mappings = {};

					frappe.meta.get_docfields(doctype).forEach((field_doc) => {
						if (is_field_editable(field_doc)) {
							field_mappings[field_doc.label] = Object.assign({}, field_doc);
						}
					});

					this.disable_list_update = true;
					bulk_operations.edit(this.get_checked_items(true), field_mappings, () => {
						this.disable_list_update = false;
						this.refresh();
					});
				},
				standard: true,
			};
		};

		const bulk_export = () => {
			return {
				label: __("Export", null, "Button in list view actions menu"),
				action: () => {
					const docnames = this.get_checked_items(true);

					bulk_operations.export(doctype, docnames);
				},
				standard: true,
			};
		};

		// bulk edit
		if (has_editable_fields(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_edit());
		}

		actions_menu_items.push(bulk_export());

		// bulk assignment
		actions_menu_items.push(bulk_assignment());

		actions_menu_items.push(bulk_assignment_rule());

		actions_menu_items.push(bulk_add_tags());

		// bulk printing
		if (frappe.model.can_print(doctype)) {
			actions_menu_items.push(bulk_printing());
		}

		// bulk submit
		if (
			frappe.model.is_submittable(doctype) &&
			has_submit_permission(doctype) &&
			!frappe.model.has_workflow(doctype)
		) {
			actions_menu_items.push(bulk_submit());
		}

		// bulk cancel
		if (frappe.model.can_cancel(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_cancel());
		}

		// bulk delete
		if (frappe.model.can_delete(doctype) && !frappe.model.has_workflow(doctype)) {
			actions_menu_items.push(bulk_delete());
		}

		return actions_menu_items;
	}

	parse_filters_from_route_options() {
		const filters = [];

		for (let field in frappe.route_options) {
			let doctype = null;
			let value = frappe.route_options[field];

			let value_array;
			if ($.isArray(value) && value[0].startsWith("[") && value[0].endsWith("]")) {
				value_array = [];
				for (var i = 0; i < value.length; i++) {
					value_array.push(JSON.parse(value[i]));
				}
			} else if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
				value = JSON.parse(value);
			}

			// if `Child DocType.fieldname`
			if (field.includes(".")) {
				doctype = field.split(".")[0];
				field = field.split(".")[1];
			}

			// find the table in which the key exists
			// for example the filter could be {"item_code": "X"}
			// where item_code is in the child table.

			// we can search all tables for mapping the doctype
			if (!doctype) {
				doctype = frappe.meta.get_doctype_for_field(this.doctype, field);
			}

			if (doctype) {
				if (value_array) {
					for (var j = 0; j < value_array.length; j++) {
						if ($.isArray(value_array[j])) {
							filters.push([doctype, field, value_array[j][0], value_array[j][1]]);
						} else {
							filters.push([doctype, field, "=", value_array[j]]);
						}
					}
				} else if ($.isArray(value)) {
					filters.push([doctype, field, value[0], value[1]]);
				} else {
					filters.push([doctype, field, "=", value]);
				}
			}
		}

		return filters;
	}
};

frappe.get_list_view = (doctype) => {
	let route = `List/${doctype}/List`;
	return frappe.views.list_view[route];
};

class ElementFactory {
	/* Pre-create templates for HTML Elements on initialization and provide them
	via the get_xxx_element methods. */
	constructor(doctype) {
		this.templates = {
			checkbox: this.create_checkbox_element(doctype),
			checkboxspan: this.create_checkboxspan_element(),
			link: this.create_link_element(doctype),
			like: this.create_like_element(doctype),
		};
	}

	create_checkbox_element(doctype) {
		const checkbox = document.createElement("input");
		checkbox.classList.add("list-row-checkbox");
		checkbox.type = "checkbox";
		checkbox.dataset.doctype = doctype;
		return checkbox;
	}

	create_link_element(doctype) {
		const link = document.createElement("a");
		link.classList.add("ellipsis");
		link.dataset.doctype = doctype;

		return link;
	}

	create_checkboxspan_element() {
		const checkboxspan = document.createElement("span");
		checkboxspan.classList.add("level-item", "select-like");

		return checkboxspan;
	}

	create_like_element(doctype) {
		const like = document.createElement("span");
		like.classList.add("like-action");
		like.innerHTML = frappe.utils.icon("es-solid-heart", "sm", "like-icon");
		like.dataset.doctype = doctype;

		return like;
	}

	get_checkbox_element(name) {
		const checkbox = this.templates.checkbox.cloneNode(true);
		checkbox.dataset.name = name;
		return checkbox;
	}

	get_checkboxspan_element() {
		return this.templates.checkboxspan.cloneNode(true);
	}

	get_link_element(name, href, text) {
		const link = this.templates.link.cloneNode(true);
		link.dataset.name = name;
		link.href = href;
		link.title = text;
		link.textContent = text;

		return link;
	}

	get_like_element(name, liked, liked_by, title) {
		const like = this.templates.like.cloneNode(true);
		like.dataset.name = name;

		const heart_classes = liked ? ["liked-by", "liked"] : ["not-liked"];
		like.classList.add(...heart_classes);

		like.setAttribute("data-liked-by", liked_by || "[]");
		like.setAttribute("title", title);

		return like;
	}
}

./list_view_select.js

frappe.provide("frappe.views");

frappe.views.ListViewSelect = class ListViewSelect {
	constructor(opts) {
		$.extend(this, opts);
		this.set_current_view();
		this.setup_views();
	}

	add_view_to_menu(view, action) {
		if (this.doctype == "File" && view == "List") {
			view = "File";
		}
		let $el = this.page.add_custom_menu_item(
			this.parent,
			__(view),
			action,
			true,
			null,
			this.icon_map[view] || "list"
		);
		$el.parent().attr("data-view", view);
	}

	set_current_view() {
		this.current_view = "List";
		const route = frappe.get_route();
		const view_name = frappe.utils.to_title_case(route[2] || "");
		if (route.length > 2 && frappe.views.view_modes.includes(view_name)) {
			this.current_view = view_name;

			if (this.current_view === "Kanban") {
				this.kanban_board = route[3];
			} else if (this.current_view === "Inbox") {
				this.email_account = route[3];
			}
		}
	}

	set_route(view, calendar_name) {
		const route = [this.slug(), "view", view];
		if (calendar_name) route.push(calendar_name);

		let search_params = cur_list?.get_search_params();
		if (search_params) {
			frappe.route_options = Object.fromEntries(search_params);
		}
		frappe.set_route(route);
	}

	setup_views() {
		const views = {
			List: {
				condition: true,
				action: () => this.set_route("list"),
			},
			Report: {
				condition: true,
				action: () => this.set_route("report"),
				current_view_handler: () => {
					const reports = this.get_reports();
					let default_action = {};
					// Only add action if current route is not report builder
					if (frappe.get_route().length > 3) {
						default_action = {
							label: __("Report Builder"),
							action: () => this.set_route("report"),
						};
					}
					this.setup_dropdown_in_sidebar("Report", reports, default_action);
				},
			},
			Dashboard: {
				condition: true,
				action: () => this.set_route("dashboard"),
			},
			Calendar: {
				condition: frappe.views.calendar[this.doctype],
				action: () => this.set_route("calendar", "default"),
				current_view_handler: () => {
					this.get_calendars().then((calendars) => {
						this.setup_dropdown_in_sidebar("Calendar", calendars);
					});
				},
			},
			Gantt: {
				condition: frappe.views.calendar[this.doctype],
				action: () => this.set_route("gantt"),
			},
			Inbox: {
				condition: this.doctype === "Communication" && frappe.boot.email_accounts.length,
				action: () => this.set_route("inbox"),
				current_view_handler: () => {
					const accounts = this.get_email_accounts();
					let default_action;
					if (has_common(frappe.user_roles, ["System Manager", "Administrator"])) {
						default_action = {
							label: __("New Email Account"),
							action: () => frappe.new_doc("Email Account"),
						};
					}
					this.setup_dropdown_in_sidebar("Inbox", accounts, default_action);
				},
			},
			Image: {
				condition: this.list_view.meta.image_field,
				action: () => this.set_route("image"),
			},
			Tree: {
				condition:
					frappe.treeview_settings[this.doctype] ||
					frappe.get_meta(this.doctype).is_tree,
				action: () => this.set_route("tree"),
			},
			Kanban: {
				condition: this.doctype != "File",
				action: () => this.setup_kanban_boards(),
				current_view_handler: () => {
					frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) =>
						this.setup_kanban_switcher(kanbans)
					);
				},
			},
			Map: {
				condition:
					this.list_view.settings.get_coords_method ||
					(this.list_view.meta.fields.find((i) => i.fieldname === "latitude") &&
						this.list_view.meta.fields.find((i) => i.fieldname === "longitude")) ||
					this.list_view.meta.fields.find(
						(i) => i.fieldname === "location" && i.fieldtype == "Geolocation"
					),
				action: () => this.set_route("map"),
			},
		};

		frappe.views.view_modes.forEach((view) => {
			if (this.current_view !== view && views[view].condition) {
				this.add_view_to_menu(view, views[view].action);
			}

			if (this.current_view == view) {
				views[view].current_view_handler && views[view].current_view_handler();
			}
		});
	}

	setup_dropdown_in_sidebar(view, items, default_action) {
		if (!this.sidebar) return;
		const views_wrapper = this.sidebar.sidebar.find(".views-section");
		views_wrapper.find(".sidebar-label").html(__(view));
		const $dropdown = views_wrapper.find(".views-dropdown");

		let placeholder = __("Select {0}", [__(view)]);
		let html = ``;

		if (!items || !items.length) {
			html = `<div class="empty-state">
						${__("No {0} Found", [__(view)])}
				</div>`;
		} else {
			const page_name = this.get_page_name();
			items.map((item) => {
				if (item.name.toLowerCase() == page_name.toLowerCase()) {
					placeholder = item.name;
				} else {
					html += `<li><a class="dropdown-item" href="${item.route}">${item.name}</a></li>`;
				}
			});
		}

		views_wrapper.find(".selected-view").html(placeholder);

		if (default_action) {
			views_wrapper.find(".sidebar-action a").html(default_action.label);
			views_wrapper.find(".sidebar-action a").click(() => default_action.action());
		}

		$dropdown.html(html);

		views_wrapper.removeClass("hide");
	}

	setup_kanban_switcher(kanbans) {
		const kanban_switcher = this.page.add_custom_button_group(
			__("Select Kanban"),
			null,
			this.list_view.$filter_section
		);

		kanbans.map((k) => {
			this.page.add_custom_menu_item(
				kanban_switcher,
				k.name,
				() => this.set_route("kanban", k.name),
				false
			);
		});

		let perms = this.list_view.board_perms;
		let can_create = perms ? perms.create : true;
		if (can_create) {
			this.page.add_custom_menu_item(
				kanban_switcher,
				__("Create New Kanban Board"),
				() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
				true
			);
		}
	}

	get_page_name() {
		return frappe.utils.to_title_case(frappe.get_route().slice(-1)[0] || "");
	}

	get_reports() {
		// add reports linked to this doctype to the dropdown
		let added = [];
		let reports_to_add = [];

		let add_reports = (reports) => {
			reports.map((r) => {
				if (!r.ref_doctype || r.ref_doctype == this.doctype) {
					const report_type =
						r.report_type === "Report Builder"
							? `/app/list/${r.ref_doctype}/report`
							: "/app/query-report";

					const route = r.route || report_type + "/" + (r.title || r.name);

					if (added.indexOf(route) === -1) {
						// don't repeat
						added.push(route);
						reports_to_add.push({
							name: __(r.title || r.name),
							route: route,
						});
					}
				}
			});
		};

		// from reference doctype
		if (this.list_view.settings.reports) {
			add_reports(this.list_view.settings.reports);
		}

		// Sort reports alphabetically
		var reports =
			Object.values(frappe.boot.user.all_reports).sort((a, b) =>
				a.title.localeCompare(b.title)
			) || [];

		// from specially tagged reports
		add_reports(reports);

		return reports_to_add;
	}

	setup_kanban_boards() {
		function fetch_kanban_board(doctype) {
			frappe.db.get_value(
				"Kanban Board",
				{ reference_doctype: doctype },
				"name",
				(board) => {
					if (!$.isEmptyObject(board)) {
						frappe.set_route("list", doctype, "kanban", board.name);
					} else {
						frappe.views.KanbanView.show_kanban_dialog(doctype);
					}
				}
			);
		}

		const last_opened_kanban =
			frappe.model.user_settings[this.doctype]["Kanban"]?.last_kanban_board;
		if (!last_opened_kanban) {
			fetch_kanban_board(this.doctype);
		} else {
			frappe.db.exists("Kanban Board", last_opened_kanban).then((exists) => {
				if (exists) {
					frappe.set_route("list", this.doctype, "kanban", last_opened_kanban);
				} else {
					fetch_kanban_board(this.doctype);
				}
			});
		}
	}

	get_calendars() {
		const doctype = this.doctype;
		let calendars = [];

		return frappe.db
			.get_list("Calendar View", {
				filters: {
					reference_doctype: doctype,
				},
			})
			.then((result) => {
				if (!(result && Array.isArray(result) && result.length)) return;

				if (frappe.views.calendar[this.doctype]) {
					// has standard calendar view
					calendars.push({
						name: "Default",
						route: `/app/${this.slug()}/view/calendar/default`,
					});
				}
				result.map((calendar) => {
					calendars.push({
						name: calendar.name,
						route: `/app/${this.slug()}/view/calendar/${calendar.name}`,
					});
				});

				return calendars;
			});
	}

	get_email_accounts() {
		let accounts_to_add = [];
		let accounts = frappe.boot.email_accounts;
		accounts.forEach((account) => {
			let email_account =
				account.email_id == "All Accounts" ? "All Accounts" : account.email_account;
			let route = `/app/communication/view/inbox/${email_account}`;
			let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(
				account.email_id
			)
				? __(account.email_id)
				: account.email_account;

			accounts_to_add.push({
				name: display_name,
				route: route,
			});
		});

		return accounts_to_add;
	}

	slug() {
		return frappe.router.slug(frappe.router.doctype_layout || this.doctype);
	}
};

./list_filter.js

frappe.provide("frappe.ui");

export default class ListFilter {
	constructor({ wrapper, doctype }) {
		Object.assign(this, arguments[0]);
		this.can_add_global = frappe.user.has_role(["System Manager", "Administrator"]);
		this.filters = [];
		this.make();
		this.bind();
		this.refresh();
	}

	make() {
		// init dom
		this.wrapper.html(`
			<li class="input-area"></li>
			<li class="sidebar-action">
				<a class="saved-filters-preview">${__("Show Saved")}</a>
			</li>
			<div class="saved-filters"></div>
		`);

		this.$input_area = this.wrapper.find(".input-area");
		this.$list_filters = this.wrapper.find(".list-filters");
		this.$saved_filters = this.wrapper.find(".saved-filters").hide();
		this.$saved_filters_preview = this.wrapper.find(".saved-filters-preview");
		this.saved_filters_hidden = true;
		this.toggle_saved_filters(true);

		this.filter_input = frappe.ui.form.make_control({
			df: {
				fieldtype: "Data",
				placeholder: __("Filter Name"),
				input_class: "input-xs",
			},
			parent: this.$input_area,
			render_input: 1,
		});

		this.is_global_input = frappe.ui.form.make_control({
			df: {
				fieldtype: "Check",
				label: __("Is Global"),
			},
			parent: this.$input_area,
			render_input: 1,
		});
	}

	bind() {
		this.bind_save_filter();
		this.bind_toggle_saved_filters();
		this.bind_click_filter();
		this.bind_remove_filter();
	}

	refresh() {
		this.get_list_filters().then(() => {
			this.filters.length
				? this.$saved_filters_preview.show()
				: this.$saved_filters_preview.hide();
			const html = this.filters.map((filter) => this.filter_template(filter));
			this.wrapper.find(".filter-pill").remove();
			this.$saved_filters.append(html);
		});
		this.is_global_input.toggle(false);
		this.filter_input.set_description("");
	}

	filter_template(filter) {
		return `<div class="list-link filter-pill list-sidebar-button btn btn-default" data-name="${
			filter.name
		}">
			<a class="ellipsis filter-name">${filter.filter_name}</a>
			<a class="remove">${frappe.utils.icon("close")}</a>
		</div>`;
	}

	bind_toggle_saved_filters() {
		this.wrapper.find(".saved-filters-preview").click(() => {
			this.toggle_saved_filters(this.saved_filters_hidden);
		});
	}

	toggle_saved_filters(show) {
		this.$saved_filters.toggle(show);
		const label = show ? __("Hide Saved") : __("Show Saved");
		this.wrapper.find(".saved-filters-preview").text(label);
		this.saved_filters_hidden = !this.saved_filters_hidden;
	}

	bind_click_filter() {
		this.wrapper.on("click", ".filter-pill .filter-name", (e) => {
			let $filter = $(e.currentTarget).parent(".filter-pill");
			this.set_applied_filter($filter);
			const name = $filter.attr("data-name");
			this.list_view.filter_area.clear().then(() => {
				this.list_view.filter_area.add(this.get_filters_values(name));
			});
		});
	}

	bind_remove_filter() {
		this.wrapper.on("click", ".filter-pill .remove", (e) => {
			const $li = $(e.currentTarget).closest(".filter-pill");
			const filter_label = $li.text().trim();

			frappe.confirm(
				__("Are you sure you want to remove the {0} filter?", [filter_label.bold()]),
				() => {
					const name = $li.attr("data-name");
					const applied_filters = this.get_filters_values(name);
					$li.remove();
					this.remove_filter(name).then(() => this.refresh());
					this.list_view.filter_area.remove_filters(applied_filters);
				}
			);
		});
	}

	bind_save_filter() {
		this.filter_input.$input.keydown(
			frappe.utils.debounce((e) => {
				const value = this.filter_input.get_value();
				const has_value = Boolean(value);

				if (e.which === frappe.ui.keyCode["ENTER"]) {
					if (!has_value || this.filter_name_exists(value)) return;

					this.filter_input.set_value("");
					this.save_filter(value).then(() => this.refresh());
					this.toggle_saved_filters(true);
				} else {
					let help_text = __("Press Enter to save");

					if (this.filter_name_exists(value)) {
						help_text = __("Duplicate Filter Name");
					}

					this.filter_input.set_description(has_value ? help_text : "");

					if (this.can_add_global) {
						this.is_global_input.toggle(has_value);
					}
				}
			}, 300)
		);
	}

	save_filter(filter_name) {
		return frappe.db.insert({
			doctype: "List Filter",
			reference_doctype: this.list_view.doctype,
			filter_name,
			for_user: this.is_global_input.get_value() ? "" : frappe.session.user,
			filters: JSON.stringify(this.get_current_filters()),
		});
	}

	remove_filter(name) {
		if (!name) return;
		return frappe.db.delete_doc("List Filter", name);
	}

	get_filters_values(name) {
		const filter = this.filters.find((filter) => filter.name === name);
		return JSON.parse(filter.filters || "[]");
	}

	get_current_filters() {
		return this.list_view.filter_area.get();
	}

	filter_name_exists(filter_name) {
		return (this.filters || []).find((f) => f.filter_name === filter_name);
	}

	get_list_filters() {
		if (frappe.session.user === "Guest") return Promise.resolve();
		return frappe.db
			.get_list("List Filter", {
				fields: ["name", "filter_name", "for_user", "filters"],
				filters: { reference_doctype: this.list_view.doctype },
				or_filters: [
					["for_user", "=", frappe.session.user],
					["for_user", "=", ""],
				],
			})
			.then((filters) => {
				this.filters = filters || [];
			});
	}

	set_applied_filter($filter) {
		this.$saved_filters
			.find(".btn-primary-light")
			.toggleClass("btn-primary-light btn-default");
		$filter.toggleClass("btn-default btn-primary-light");
	}
}

./list_factory.js

// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.provide("frappe.views.list_view");

window.cur_list = null;
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
	make(route) {
		const me = this;
		const doctype = route[1];

		// List / Gantt / Kanban / etc
		let view_name = frappe.utils.to_title_case(route[2] || "List");

		// File is a special view
		if (doctype == "File" && !["Report", "Dashboard"].includes(view_name)) {
			view_name = "File";
		}

		let view_class = frappe.views[view_name + "View"];
		if (!view_class) view_class = frappe.views.ListView;

		if (view_class && view_class.load_last_view && view_class.load_last_view()) {
			// view can have custom routing logic
			return;
		}

		frappe.provide("frappe.views.list_view." + doctype);

		frappe.views.list_view[me.page_name] = new view_class({
			doctype: doctype,
			parent: me.make_page(true, me.page_name),
		});

		me.set_cur_list();
	}

	before_show() {
		if (this.re_route_to_view()) {
			return false;
		}

		this.set_module_breadcrumb();
	}

	on_show() {
		this.set_cur_list();
		if (cur_list) cur_list.show();
	}

	re_route_to_view() {
		const doctype = this.route[1];
		const last_route = frappe.route_history.slice(-2)[0];
		if (
			this.route[0] === "List" &&
			this.route.length === 2 &&
			frappe.views.list_view[doctype] &&
			last_route &&
			last_route[0] === "List" &&
			last_route[1] === doctype
		) {
			// last route same as this route, so going back.
			// this happens because /app/List/Item will redirect to /app/List/Item/List
			// while coming from back button, the last 2 routes will be same, so
			// we know user is coming in the reverse direction (via back button)

			// example:
			// Step 1: /app/List/Item redirects to /app/List/Item/List
			// Step 2: User hits "back" comes back to /app/List/Item
			// Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
			window.history.go(-1);
			return true;
		}
	}

	set_module_breadcrumb() {
		if (frappe.route_history.length > 1) {
			const prev_route = frappe.route_history[frappe.route_history.length - 2];
			if (prev_route[0] === "modules") {
				const doctype = this.route[1],
					module = prev_route[1];
				if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) {
					// save the last page from the breadcrumb was accessed
					frappe.breadcrumbs.set_doctype_module(doctype, module);
				}
			}
		}
	}

	set_cur_list() {
		cur_list = frappe.views.list_view[this.page_name];
		if (cur_list && cur_list.doctype !== this.route[1]) {
			// changing...
			window.cur_list = null;
		}
	}
};

Everything is finally bundled and made available via below code at ./apps/frappe/frappe/public/js/list.bundle.js

import "./frappe/ui/listing.html";

import "./frappe/model/indicator.js";
import "./frappe/ui/filters/filter.js";
import "./frappe/ui/filters/filter_list.js";
import "./frappe/ui/filters/field_select.js";
import "./frappe/ui/filters/edit_filter.html";
import "./frappe/ui/tags.js";
import "./frappe/ui/tag_editor.js";
import "./frappe/ui/like.js";
import "../html/print_template.html";

import "./frappe/list/base_list.js";
import "./frappe/list/list_view.js";
import "./frappe/list/list_factory.js";

import "./frappe/list/list_view_select.js";
import "./frappe/list/list_sidebar.js";
import "./frappe/list/list_sidebar.html";
import "./frappe/list/list_sidebar_stat.html";
import "./frappe/list/list_sidebar_group_by.js";
import "./frappe/list/list_view_permission_restrictions.html";

import "./frappe/views/gantt/gantt_view.js";
import "./frappe/views/calendar/calendar.js";
import "./frappe/views/dashboard/dashboard_view.js";
import "./frappe/views/image/image_view.js";
import "./frappe/views/map/map_view.js";
import "./frappe/views/kanban/kanban_view.js";
import "./frappe/views/inbox/inbox_view.js";
import "./frappe/views/file/file_view.js";

import "./frappe/views/treeview.js";
import "./frappe/views/interaction.js";

import "./frappe/views/image/image_view_item_row.html";
import "./frappe/views/image/photoswipe_dom.html";

import "./frappe/views/kanban/kanban_board.html";
import "./frappe/views/kanban/kanban_column.html";
import "./frappe/views/kanban/kanban_card.html";

Which is then included in hooks.py of frappe app as below:

website

app_include_js = [
	"list.bundle.js",
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment