From 721273a2f7780de4c3dd0a362843b22679be6cdd Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Mon, 22 Jun 2026 09:29:27 +0000 Subject: [PATCH] feat: block DocTypes via the real /app/doctype list (edit mode only) Replaces the search-only dialog with the native DocType list: the 'Block DocTypes' editor action routes to /app/doctype, where a 'Jey Layout' button group (Block/Unblock selected, Show blocked only toggle) and a 'Blocked' indicator are injected via listview_settings (extends, never overwrites core). Buttons/indicator show only in edit mode; robust to the dynamic-load race by also applying to the live cur_list on route change. Server helpers block_doctypes/unblock_doctypes/blocked_doctypes_list. Banner shortened and hidden off workspace pages so it no longer covers the list toolbar. Co-Authored-By: Claude Opus 4.8 --- jey_layout/api.py | 52 +++++++- jey_layout/public/js/jey_layout.js | 183 +++++++++++++++++++++-------- 2 files changed, 178 insertions(+), 57 deletions(-) diff --git a/jey_layout/api.py b/jey_layout/api.py index 45591dc..b0b5a4c 100644 --- a/jey_layout/api.py +++ b/jey_layout/api.py @@ -245,9 +245,7 @@ def save_config(config): config = json.loads(config) cfg = _validate_config(config) _write(desk_layout.working_path(), cfg) - # Drop the per-request cache so a follow-up preview request reflects the save. - if hasattr(frappe.local, desk_layout._CACHE_ATTR): - delattr(frappe.local, desk_layout._CACHE_ATTR) + _bust_caches() # so a follow-up request reflects the save (hide/block + structural) return {"ok": True} @@ -277,6 +275,50 @@ def reset_to_default(): _guard() cfg = desk_layout._read_file(desk_layout.snapshot_path()) _write(desk_layout.working_path(), _validate_config(cfg or {"workspaces": {}})) - if hasattr(frappe.local, desk_layout._CACHE_ATTR): - delattr(frappe.local, desk_layout._CACHE_ATTR) + _bust_caches() return {"ok": True} + + +def _bust_caches(): + for attr in (desk_layout._CACHE_ATTR, "_jey_layout_raw"): + if hasattr(frappe.local, attr): + delattr(frappe.local, attr) + + +@frappe.whitelist() +def blocked_doctypes_list(): + """Current global block list — used by the DocType list-view extension.""" + _guard() + cfg = _read_working() or {} + return [d for d in (cfg.get("blocked_doctypes") or []) if isinstance(d, str)] + + +@frappe.whitelist() +def block_doctypes(names): + return _toggle_blocked(names, True) + + +@frappe.whitelist() +def unblock_doctypes(names): + return _toggle_blocked(names, False) + + +def _toggle_blocked(names, block: bool): + """Add/remove doctypes from the global block list (read-modify-write).""" + _guard() + if isinstance(names, str): + names = frappe.parse_json(names) + names = [n for n in (names or []) if isinstance(n, str) and n] + cfg = _read_working() or {"version": 1, "workspaces": {}} + cur = [n for n in (cfg.get("blocked_doctypes") or []) if isinstance(n, str)] + if block: + for n in names: + if n not in cur: + cur.append(n) + else: + drop = set(names) + cur = [n for n in cur if n not in drop] + cfg["blocked_doctypes"] = cur + _write(desk_layout.working_path(), _validate_config(cfg)) + _bust_caches() + return {"blocked_doctypes": cur} diff --git a/jey_layout/public/js/jey_layout.js b/jey_layout/public/js/jey_layout.js index d63ab24..6b8d8d9 100644 --- a/jey_layout/public/js/jey_layout.js +++ b/jey_layout/public/js/jey_layout.js @@ -35,6 +35,7 @@ let hb_timer = null; let decorating = false; let saved_timer = null; + let blocked_set = new Set(); // current global blocked_doctypes (for the DocType list) function is_admin() { return frappe.user && frappe.user.has_role && frappe.user.has_role("System Manager"); @@ -62,6 +63,8 @@ mount_fab(); update_fab(); update_banner(); + install_doctype_list_actions(); + ensure_doctype_list_extras(); // in case we loaded straight onto the DocType list if (editing) { $("body").addClass("jey-edit"); @@ -70,6 +73,8 @@ } frappe.router.on("change", () => { + update_banner(); // hide/show per page (don't cover toolbars off the desk) + ensure_doctype_list_extras(); // re-add buttons when (re)entering the DocType list if (editing) decorate(); }); } @@ -92,62 +97,130 @@ `).appendTo("body"); $fab.find(".jey-fab-btn").on("click", toggle_edit); - $fab.find('[data-act="block"]').on("click", open_block_doctypes_dialog); + $fab.find('[data-act="block"]').on("click", open_block_doctypes); $fab.find('[data-act="export"]').on("click", do_export); $fab.find('[data-act="reset"]').on("click", do_reset); } - // Global block list — block any doctype, including ones not present on /desk. - function open_block_doctypes_dialog() { - let list = (config.blocked_doctypes || []).slice(); - const d = new frappe.ui.Dialog({ - title: __("Block DocTypes (anywhere, incl. not on the desk)"), - fields: [ - { fieldname: "add", label: __("Add a DocType to block"), fieldtype: "Link", options: "DocType" }, - { fieldname: "list_html", fieldtype: "HTML" }, - ], - primary_action_label: __("Save"), - primary_action: async () => { - if (list.length) config.blocked_doctypes = list; - else delete config.blocked_doctypes; - await save_now(); - d.hide(); - frappe.show_alert( - { message: __("Saved — “✓ Done editing” to apply the block"), indicator: "green" }, - 5 - ); + // Block DocTypes via the REAL /app/doctype list (native search/filters/checkboxes). + function open_block_doctypes() { + frappe.set_route("List", "DocType"); + frappe.show_alert( + { + message: __("Tick DocTypes, then “Jey Layout → Block selected”. Leave edit mode to apply."), + indicator: "blue", }, + 7 + ); + } + + // ---- DocType list-view extension (installed once; extends, never overwrites core) ---- + async function refresh_blocked_set() { + const list = await xcall("blocked_doctypes_list"); + blocked_set = new Set(list || []); + } + + async function jey_block_selected(listview, block) { + const names = listview.get_checked_items(true) || []; + if (!names.length) { + frappe.msgprint(__("Select one or more DocTypes (row checkboxes) first.")); + return; + } + await xcall(block ? "block_doctypes" : "unblock_doctypes", { names: JSON.stringify(names) }); + await refresh_blocked_set(); + frappe.show_alert({ + message: block + ? __("Blocked {0} DocType(s) — leave edit mode to apply", [names.length]) + : __("Unblocked {0} DocType(s)", [names.length]), + indicator: block ? "red" : "blue", }); - const render = () => { - const $w = d.fields_dict.list_html.$wrapper; - $w.empty(); - if (!list.length) { - $w.html(`
${__("No blocked doctypes yet.")}
`); - return; - } - list.forEach((dt, i) => { - const $row = $( - `
🔒 ` + - `
` - ); - $row.find(".dt").text(dt); - $row.find(".jey-blocked-x").on("click", () => { - list.splice(i, 1); - render(); - }); - $w.append($row); - }); + listview.refresh(); + } + + // Idempotently add our toolbar buttons + blocked indicator to a live DocType list. + // Only while editing — outside edit mode the list stays clean. + function apply_doctype_list_extras(lv) { + if (!lv || !lv.page || !editing) return; + if (!lv.page.__jey_buttons) { + lv.page.__jey_buttons = true; + const grp = __("Jey Layout"); + lv.page.add_inner_button(__("Block selected"), () => jey_block_selected(lv, true), grp); + lv.page.add_inner_button(__("Unblock selected"), () => jey_block_selected(lv, false), grp); + let blocked_only = false; + const $toggle = lv.page.add_inner_button( + __("Show blocked only"), + () => { + try { + if (!blocked_only) { + if (!blocked_set.size) return frappe.show_alert(__("No blocked doctypes yet.")); + lv.filter_area.add([["DocType", "name", "in", Array.from(blocked_set)]]); + blocked_only = true; + $toggle.text(__("Show all DocTypes")); + } else { + lv.filter_area.remove("name"); // drop our filter → full list back + blocked_only = false; + $toggle.text(__("Show blocked only")); + } + } catch (e) {} + }, + grp + ); + } + // Patch this instance's indicator too (in case our settings registered after + // the list was already constructed — dynamic-load race). + if (lv.settings && !lv.settings.__jey_indicator) { + lv.settings.__jey_indicator = true; + const prev = lv.settings.get_indicator; + lv.settings.get_indicator = function (doc) { + if (prev) { + try { + const x = prev.call(this, doc); + if (x) return x; + } catch (e) {} + } + if (blocked_set.has(doc.name)) return [__("Blocked"), "red", "name,=," + doc.name]; + }; + } + refresh_blocked_set().then(() => lv.refresh()); + } + + // Resolve the live DocType list view and apply extras — robust to the timing + // race where our listview_settings registered after the list had rendered. + function ensure_doctype_list_extras() { + const r = (frappe.get_route && frappe.get_route()) || []; + if (!(r[0] === "List" && r[1] === "DocType")) return; + let tries = 0; + const tick = () => { + const lv = window.cur_list; + if (lv && lv.doctype === "DocType" && lv.page) return apply_doctype_list_extras(lv); + if (++tries < 20) setTimeout(tick, 150); }; - d.fields_dict.add.df.onchange = () => { - const v = d.get_value("add"); - if (v) { - if (!list.includes(v)) list.push(v); - d.set_value("add", ""); - render(); + tick(); + } + + function install_doctype_list_actions() { + const s = (frappe.listview_settings["DocType"] = frappe.listview_settings["DocType"] || {}); + if (s._jey_installed) return; // extend once; keep core's primary_action intact + s._jey_installed = true; + const prev_onload = s.onload; + s.onload = function (lv) { + if (prev_onload) { + try { + prev_onload.call(this, lv); + } catch (e) {} } + apply_doctype_list_extras(lv); + }; + const prev_indicator = s.get_indicator; + s.get_indicator = function (doc) { + if (prev_indicator) { + try { + const r = prev_indicator.call(this, doc); + if (r) return r; + } catch (e) {} + } + if (editing && blocked_set.has(doc.name)) return [__("Blocked"), "red", "name,=," + doc.name]; }; - render(); - d.show(); } function update_fab() { @@ -156,14 +229,20 @@ $fab.find(".jey-fab-btn").html(editing ? `✓ ${__("Done editing")}` : `✎ ${__("Edit layout")}`); } + function on_workspace_page() { + const r = (frappe.get_route && frappe.get_route()) || []; + return r.length === 0 || r[0] === "Workspaces"; + } + function update_banner() { let $b = $("#jey-banner"); if (!$b.length) $b = $('
').appendTo("body"); - if (editing) { - $b.attr("class", "edit").html( - `✎ ${__("EDIT MODE")} — ${__("all items shown")}; 👁 ${__("visible")} · 🚫 ${__("hidden")} · 🔒 ${__("blocked")}. ` + - `${__("Click “✓ Done editing” to see the applied result.")}` - ).show(); + // Only on workspace pages — elsewhere (e.g. the DocType list) it would + // overlap the page toolbar and hide the "Jey Layout" buttons. + if (editing && on_workspace_page()) { + $b.attr("class", "edit") + .html(`✎ ${__("EDIT MODE")} — 👁 ${__("visible")} · 🚫 ${__("hidden")} · 🔒 ${__("blocked")}`) + .show(); } else { $b.hide(); }