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 <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-22 09:29:27 +00:00
parent b41d517249
commit 721273a2f7
2 changed files with 178 additions and 57 deletions

View File

@ -245,9 +245,7 @@ def save_config(config):
config = json.loads(config) config = json.loads(config)
cfg = _validate_config(config) cfg = _validate_config(config)
_write(desk_layout.working_path(), cfg) _write(desk_layout.working_path(), cfg)
# Drop the per-request cache so a follow-up preview request reflects the save. _bust_caches() # so a follow-up request reflects the save (hide/block + structural)
if hasattr(frappe.local, desk_layout._CACHE_ATTR):
delattr(frappe.local, desk_layout._CACHE_ATTR)
return {"ok": True} return {"ok": True}
@ -277,6 +275,50 @@ def reset_to_default():
_guard() _guard()
cfg = desk_layout._read_file(desk_layout.snapshot_path()) cfg = desk_layout._read_file(desk_layout.snapshot_path())
_write(desk_layout.working_path(), _validate_config(cfg or {"workspaces": {}})) _write(desk_layout.working_path(), _validate_config(cfg or {"workspaces": {}}))
if hasattr(frappe.local, desk_layout._CACHE_ATTR): _bust_caches()
delattr(frappe.local, desk_layout._CACHE_ATTR)
return {"ok": True} 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}

View File

@ -35,6 +35,7 @@
let hb_timer = null; let hb_timer = null;
let decorating = false; let decorating = false;
let saved_timer = null; let saved_timer = null;
let blocked_set = new Set(); // current global blocked_doctypes (for the DocType list)
function is_admin() { function is_admin() {
return frappe.user && frappe.user.has_role && frappe.user.has_role("System Manager"); return frappe.user && frappe.user.has_role && frappe.user.has_role("System Manager");
@ -62,6 +63,8 @@
mount_fab(); mount_fab();
update_fab(); update_fab();
update_banner(); update_banner();
install_doctype_list_actions();
ensure_doctype_list_extras(); // in case we loaded straight onto the DocType list
if (editing) { if (editing) {
$("body").addClass("jey-edit"); $("body").addClass("jey-edit");
@ -70,6 +73,8 @@
} }
frappe.router.on("change", () => { 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(); if (editing) decorate();
}); });
} }
@ -92,62 +97,130 @@
`).appendTo("body"); `).appendTo("body");
$fab.find(".jey-fab-btn").on("click", toggle_edit); $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="export"]').on("click", do_export);
$fab.find('[data-act="reset"]').on("click", do_reset); $fab.find('[data-act="reset"]').on("click", do_reset);
} }
// Global block list — block any doctype, including ones not present on /desk. // Block DocTypes via the REAL /app/doctype list (native search/filters/checkboxes).
function open_block_doctypes_dialog() { function open_block_doctypes() {
let list = (config.blocked_doctypes || []).slice(); frappe.set_route("List", "DocType");
const d = new frappe.ui.Dialog({ frappe.show_alert(
title: __("Block DocTypes (anywhere, incl. not on the desk)"), {
fields: [ message: __("Tick DocTypes, then “Jey Layout → Block selected”. Leave edit mode to apply."),
{ fieldname: "add", label: __("Add a DocType to block"), fieldtype: "Link", options: "DocType" }, indicator: "blue",
{ 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
);
}, },
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 = () => { listview.refresh();
const $w = d.fields_dict.list_html.$wrapper; }
$w.empty();
if (!list.length) { // Idempotently add our toolbar buttons + blocked indicator to a live DocType list.
$w.html(`<div class="text-muted" style="padding:6px 0">${__("No blocked doctypes yet.")}</div>`); // Only while editing — outside edit mode the list stays clean.
return; function apply_doctype_list_extras(lv) {
} if (!lv || !lv.page || !editing) return;
list.forEach((dt, i) => { if (!lv.page.__jey_buttons) {
const $row = $( lv.page.__jey_buttons = true;
`<div class="jey-blocked-row">🔒 <span class="dt"></span>` + const grp = __("Jey Layout");
`<button type="button" class="jey-blocked-x" title="${__("Remove")}">✕</button></div>` 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);
$row.find(".dt").text(dt); let blocked_only = false;
$row.find(".jey-blocked-x").on("click", () => { const $toggle = lv.page.add_inner_button(
list.splice(i, 1); __("Show blocked only"),
render(); () => {
}); try {
$w.append($row); 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 = () => { tick();
const v = d.get_value("add"); }
if (v) {
if (!list.includes(v)) list.push(v); function install_doctype_list_actions() {
d.set_value("add", ""); const s = (frappe.listview_settings["DocType"] = frappe.listview_settings["DocType"] || {});
render(); 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() { function update_fab() {
@ -156,14 +229,20 @@
$fab.find(".jey-fab-btn").html(editing ? `${__("Done editing")}` : `${__("Edit layout")}`); $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() { function update_banner() {
let $b = $("#jey-banner"); let $b = $("#jey-banner");
if (!$b.length) $b = $('<div id="jey-banner"></div>').appendTo("body"); if (!$b.length) $b = $('<div id="jey-banner"></div>').appendTo("body");
if (editing) { // Only on workspace pages — elsewhere (e.g. the DocType list) it would
$b.attr("class", "edit").html( // overlap the page toolbar and hide the "Jey Layout" buttons.
`✎ <b>${__("EDIT MODE")}</b> — ${__("all items shown")}; 👁 ${__("visible")} · 🚫 ${__("hidden")} · 🔒 ${__("blocked")}. ` + if (editing && on_workspace_page()) {
`${__("Click “✓ Done editing” to see the applied result.")}` $b.attr("class", "edit")
).show(); .html(`✎ <b>${__("EDIT MODE")}</b> — 👁 ${__("visible")} · 🚫 ${__("hidden")} · 🔒 ${__("blocked")}`)
.show();
} else { } else {
$b.hide(); $b.hide();
} }