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:
parent
b41d517249
commit
721273a2f7
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue