perf(assets): serve desk js/css as hashed bundles

Rename custom_subtabs.js/.css to .bundle.js/.bundle.css so esbuild emits
content-hashed filenames → automatic cache-busting (no more nginx 1-year
stale-asset issue on edits). setup_custom_subtabs is defined and called
within the file (verified self-contained), so esbuild's IIFE wrap is safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-24 10:28:31 +00:00
parent c5e637fef2
commit 7d9fb69f3b
9 changed files with 357 additions and 2 deletions

View File

@ -25,8 +25,9 @@ app_license = "mit"
# ------------------ # ------------------
# include js, css files in header of desk.html # include js, css files in header of desk.html
app_include_css = "/assets/custom_subtabs/css/custom_subtabs.css" # Hashed bundles → automatic cache-busting (no nginx 1-year stale-asset issue).
app_include_js = "/assets/custom_subtabs/js/custom_subtabs.js" app_include_css = "custom_subtabs.bundle.css"
app_include_js = "custom_subtabs.bundle.js"
# include js, css files in header of web template # include js, css files in header of web template
# web_include_css = "/assets/custom_subtabs/css/custom_subtabs.css" # web_include_css = "/assets/custom_subtabs/css/custom_subtabs.css"

View File

@ -0,0 +1,68 @@
/* ../../../../../tmp/tmp-18495-vJNc0fl1RN65/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css */
.sub-tab-container {
margin: 0 !important;
padding: 0 !important;
display: flex !important;
justify-content: flex-start !important;
border-bottom: 1px solid #ddd !important;
}
.sub-tab-container .sub-tab {
margin: 0 !important;
background: none !important;
border: none !important;
padding: 0 !important;
}
.sub-tab-container .sub-tab a,
.sub-tab-container .sub-tab button {
text-decoration: none !important;
display: block !important;
padding: 6px 15px !important;
color: #555 !important;
border: none !important;
font-size: 14px !important;
font-weight: normal !important;
margin: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.sub-tab-container .sub-tab a:hover,
.sub-tab-container .sub-tab button:hover {
color: #000 !important;
}
.sub-tab-container .sub-tab.active a,
.sub-tab-container .sub-tab.active button {
color: #000 !important;
font-weight: 600 !important;
border-bottom: 2px solid #000 !important;
background-color: transparent !important;
border-radius: 0 !important;
}
.form-section:has(.sub-tab-container) {
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
.form-section .section-body:empty,
.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child) {
display: none !important;
padding: 0 !important;
margin: 0 !important;
min-height: 0 !important;
}
.sub-tab-container.nav {
display: flex;
list-style: none;
padding: 0;
margin: 0;
}
.sub-tab-container.nav > li[style*="display: none"] {
display: none !important;
}
.sub-tab-container.subtab-hidden {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
height: 0 !important;
overflow: hidden !important;
}
/*# sourceMappingURL=custom_subtabs.bundle.HFXJTVZI.css.map */

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../../../../../../tmp/tmp-18495-vJNc0fl1RN65/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
"sourcesContent": [".sub-tab-container {\n margin: 0 !important;\n padding: 0 !important;\n display: flex !important;\n justify-content: flex-start !important;\n border-bottom: 1px solid #ddd !important;\n}\n\n.sub-tab-container .sub-tab {\n margin: 0 !important;\n background: none !important;\n border: none !important;\n padding: 0 !important;\n}\n\n/* Support both <a> and <button> for v15 and v16 compatibility */\n.sub-tab-container .sub-tab a,\n.sub-tab-container .sub-tab button {\n text-decoration: none !important;\n display: block !important;\n padding: 6px 15px !important;\n color: #555 !important;\n border: none !important;\n font-size: 14px !important;\n font-weight: normal !important;\n margin: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n}\n\n.sub-tab-container .sub-tab a:hover,\n.sub-tab-container .sub-tab button:hover {\n color: #000 !important;\n}\n\n.sub-tab-container .sub-tab.active a,\n.sub-tab-container .sub-tab.active button {\n color: #000 !important;\n font-weight: 600 !important;\n border-bottom: 2px solid #000 !important;\n background-color: transparent !important;\n border-radius: 0 !important;\n}\n\n/* \u041A\u041B\u042E\u0427\u0415\u0412\u041E\u0415 \u0418\u0421\u041F\u0420\u0410\u0412\u041B\u0415\u041D\u0418\u0415: \u0443\u0431\u0440\u0430\u0442\u044C padding \u0443 form-section \u0441 \u0441\u0443\u0431\u0442\u0430\u0431\u0430\u043C\u0438 */\n.form-section:has(.sub-tab-container) {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-bottom: 0 !important;\n}\n\n/* \u0421\u043A\u0440\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u0435 section-body */\n.form-section .section-body:empty,\n.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname=\"html_vohk\"] > span.tooltip-content:only-child) {\n display: none !important;\n padding: 0 !important;\n margin: 0 !important;\n min-height: 0 !important;\n}\n\n/* \u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 sub-tab-container */\n.sub-tab-container.nav {\n display: flex;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.sub-tab-container.nav > li[style*=\"display: none\"] {\n display: none !important;\n}\n\n/* \u041A\u043B\u0430\u0441\u0441 \u0434\u043B\u044F \u0441\u043A\u0440\u044B\u0442\u0438\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0441\u0443\u0431\u0442\u0430\u0431\u043E\u0432 */\n.sub-tab-container.subtab-hidden {\n display: none !important;\n visibility: hidden !important;\n pointer-events: none !important;\n height: 0 !important;\n overflow: hidden !important;\n}"],
"mappings": ";AAAA;AACI;AACA;AACA;AACA;AACA;AAAA;AAGJ;AACI;AACA;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAGJ;AAAA;AAEI;AAAA;AAGJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AACA;AAAA;AAGJ;AACI;AAAA;AAIJ;AACI;AACA;AACA;AACA;AACA;AAAA;",
"names": []
}

View File

@ -0,0 +1,68 @@
/* ../../../../../tmp/tmp-18495-RhkE9CzEb0G6/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css */
.sub-tab-container {
margin: 0 !important;
padding: 0 !important;
display: flex !important;
justify-content: flex-start !important;
border-bottom: 1px solid #ddd !important;
}
.sub-tab-container .sub-tab {
margin: 0 !important;
background: none !important;
border: none !important;
padding: 0 !important;
}
.sub-tab-container .sub-tab a,
.sub-tab-container .sub-tab button {
text-decoration: none !important;
display: block !important;
padding: 6px 15px !important;
color: #555 !important;
border: none !important;
font-size: 14px !important;
font-weight: normal !important;
margin: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.sub-tab-container .sub-tab a:hover,
.sub-tab-container .sub-tab button:hover {
color: #000 !important;
}
.sub-tab-container .sub-tab.active a,
.sub-tab-container .sub-tab.active button {
color: #000 !important;
font-weight: 600 !important;
border-bottom: 2px solid #000 !important;
background-color: transparent !important;
border-radius: 0 !important;
}
.form-section:has(.sub-tab-container) {
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
.form-section .section-body:empty,
.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child) {
display: none !important;
padding: 0 !important;
margin: 0 !important;
min-height: 0 !important;
}
.sub-tab-container.nav {
display: flex;
list-style: none;
padding: 0;
margin: 0;
}
.sub-tab-container.nav > li[style*="display: none"] {
display: none !important;
}
.sub-tab-container.subtab-hidden {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
height: 0 !important;
overflow: hidden !important;
}
/*# sourceMappingURL=custom_subtabs.bundle.6DBL7IOJ.css.map */

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../../../../../../tmp/tmp-18495-RhkE9CzEb0G6/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
"sourcesContent": [".sub-tab-container {\n margin: 0 !important;\n padding: 0 !important;\n display: flex !important;\n justify-content: flex-start !important;\n border-bottom: 1px solid #ddd !important;\n}\n\n.sub-tab-container .sub-tab {\n margin: 0 !important;\n background: none !important;\n border: none !important;\n padding: 0 !important;\n}\n\n/* Support both <a> and <button> for v15 and v16 compatibility */\n.sub-tab-container .sub-tab a,\n.sub-tab-container .sub-tab button {\n text-decoration: none !important;\n display: block !important;\n padding: 6px 15px !important;\n color: #555 !important;\n border: none !important;\n font-size: 14px !important;\n font-weight: normal !important;\n margin: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n}\n\n.sub-tab-container .sub-tab a:hover,\n.sub-tab-container .sub-tab button:hover {\n color: #000 !important;\n}\n\n.sub-tab-container .sub-tab.active a,\n.sub-tab-container .sub-tab.active button {\n color: #000 !important;\n font-weight: 600 !important;\n border-bottom: 2px solid #000 !important;\n background-color: transparent !important;\n border-radius: 0 !important;\n}\n\n/* \u041A\u041B\u042E\u0427\u0415\u0412\u041E\u0415 \u0418\u0421\u041F\u0420\u0410\u0412\u041B\u0415\u041D\u0418\u0415: \u0443\u0431\u0440\u0430\u0442\u044C padding \u0443 form-section \u0441 \u0441\u0443\u0431\u0442\u0430\u0431\u0430\u043C\u0438 */\n.form-section:has(.sub-tab-container) {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-bottom: 0 !important;\n}\n\n/* \u0421\u043A\u0440\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u0435 section-body */\n.form-section .section-body:empty,\n.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname=\"html_vohk\"] > span.tooltip-content:only-child) {\n display: none !important;\n padding: 0 !important;\n margin: 0 !important;\n min-height: 0 !important;\n}\n\n/* \u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 sub-tab-container */\n.sub-tab-container.nav {\n display: flex;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.sub-tab-container.nav > li[style*=\"display: none\"] {\n display: none !important;\n}\n\n/* \u041A\u043B\u0430\u0441\u0441 \u0434\u043B\u044F \u0441\u043A\u0440\u044B\u0442\u0438\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0441\u0443\u0431\u0442\u0430\u0431\u043E\u0432 */\n.sub-tab-container.subtab-hidden {\n display: none !important;\n visibility: hidden !important;\n pointer-events: none !important;\n height: 0 !important;\n overflow: hidden !important;\n}"],
"mappings": ";AAAA;AACI;AACA;AACA;AACA;AACA;AAAA;AAGJ;AACI;AACA;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAGJ;AAAA;AAEI;AAAA;AAGJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AACA;AAAA;AAGJ;AACI;AAAA;AAIJ;AACI;AACA;AACA;AACA;AACA;AAAA;",
"names": []
}

View File

@ -0,0 +1,197 @@
(() => {
// ../custom_subtabs/custom_subtabs/public/js/custom_subtabs.bundle.js
function setup_custom_subtabs(wrapper, currentDoctype) {
var _a;
if (!wrapper || !wrapper.length) {
return;
}
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
const hierarchy = (_a = frappe.boot.tab_hierarchy) == null ? void 0 : _a[currentDoctype];
if (!hierarchy) {
return;
}
const subtabs = hierarchy.subtabs || {};
if (Object.keys(subtabs).length === 0) {
return;
}
const tr = (s) => typeof __ === "function" ? __(s) : s;
function getTabLink(tabElement) {
const $tab = $(tabElement);
let link = $tab.find("button.nav-link");
if (!link.length) {
link = $tab.find("a.nav-link");
}
return link;
}
if (wrapper.attr("data-subtabs-processed") === "true") {
return;
}
const childToParentMap = {};
for (const [parentTab, children] of Object.entries(subtabs)) {
children.forEach((child) => {
childToParentMap[child] = parentTab;
});
}
tabs.each((index, tab) => {
const tabLabel = $(tab).text().trim();
const parentKey = Object.keys(subtabs).find((k) => tr(k) === tabLabel);
if (parentKey) {
const tabLink = getTabLink(tab);
const tabContentId = tabLink.attr("aria-controls");
const tabContent = $(`#${tabContentId}`);
if (!tabContent.length) {
return;
}
let sectionContainer = tabContent.find(".form-section.card-section");
if (!sectionContainer.length) {
sectionContainer = tabContent.find(".form-section");
}
if (!sectionContainer.length) {
sectionContainer = tabContent.children().first();
}
if (!sectionContainer.length) {
sectionContainer = tabContent;
}
let subtabContainer = sectionContainer.find(".sub-tab-container");
if (!subtabContainer.length) {
subtabContainer = $("<div class='sub-tab-container nav' data-parent-id='" + tabContentId + "'></div>");
sectionContainer.prepend(subtabContainer);
}
subtabs[parentKey].forEach((subtab) => {
const subtabElement = tabs.filter((_, el) => $(el).text().trim() === tr(subtab));
if (subtabElement.length) {
const subtabClone = subtabElement.clone(true);
subtabClone.addClass("sub-tab");
subtabClone.attr("data-subtab-name", subtab);
const clonedLink = getTabLink(subtabClone);
clonedLink.removeAttr("href");
clonedLink.attr("data-target-id", clonedLink.attr("aria-controls"));
clonedLink.off("click");
subtabClone.attr("data-parent-id", tabContentId);
subtabContainer.append(subtabClone);
subtabElement.remove();
}
});
}
});
function ensureParentChainVisible(subtabName) {
let currentChild = subtabName;
const parentsToShow = [];
while (childToParentMap[currentChild]) {
const parentName = childToParentMap[currentChild];
parentsToShow.push(parentName);
const siblings = subtabs[parentName] || [];
siblings.forEach((sibling) => {
const siblingElement = wrapper.find(`.sub-tab[data-subtab-name="${sibling}"]`);
siblingElement.css("display", "");
});
currentChild = parentName;
}
parentsToShow.forEach((parentName) => {
const parentTabContentId = `${currentDoctype.toLowerCase().replace(/\s+/g, "-")}-${parentName.toLowerCase().replace(/\s+/g, "_")}_tab`;
const parentContent = $(`#${parentTabContentId}`);
if (parentContent.length && !parentContent.hasClass("active")) {
parentContent.addClass("active show").css("display", "block");
}
});
}
wrapper.on("click.customsubtabs", ".sub-tab button.nav-link, .sub-tab a.nav-link", function(event) {
event.preventDefault();
event.stopPropagation();
const clickedButton = $(this);
const clickedSubtab = clickedButton.closest(".sub-tab");
const subtabName = clickedSubtab.attr("data-subtab-name");
const targetId = clickedButton.attr("data-target-id") || clickedButton.attr("aria-controls");
const parentContainerId = clickedSubtab.attr("data-parent-id");
wrapper.find(".nav-item").removeClass("active");
wrapper.find("button.nav-link, a.nav-link").removeClass("active").attr("aria-selected", "false");
wrapper.find(".tab-pane").removeClass("active show");
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId !== parentContainerId) {
$(this).addClass("subtab-hidden");
} else {
$(this).removeClass("subtab-hidden");
}
});
clickedSubtab.addClass("active");
clickedButton.addClass("active").attr("aria-selected", "true");
const targetPane = $(`#${targetId}`);
if (targetPane.length) {
targetPane.addClass("active show");
}
const parentPane = $(`#${parentContainerId}`);
if (parentPane.length) {
parentPane.addClass("active show").css("display", "block");
if (targetPane.length) {
parentPane.after(targetPane);
}
}
ensureParentChainVisible(subtabName);
});
function setupMainTabHandler() {
if (wrapper.data("subtabs-observer")) {
wrapper.data("subtabs-observer").disconnect();
}
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const button = mutation.target;
const $button = $(button);
if ($button.hasClass("nav-link") && $button.hasClass("active")) {
const li = $button.closest("li.nav-item");
if (li.hasClass("sub-tab")) {
return;
}
const tabId = button.getAttribute("aria-controls");
wrapper.find(".sub-tab").removeClass("active");
wrapper.find(".sub-tab button.nav-link, .sub-tab a.nav-link").removeClass("active").attr("aria-selected", "false");
wrapper.find(".sub-tab").each(function() {
const subtabButton = getTabLink($(this));
const subtabContentId = subtabButton.attr("data-target-id") || subtabButton.attr("aria-controls");
if (subtabContentId) {
$(`#${subtabContentId}`).removeClass("active show");
}
});
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId === tabId) {
$(this).removeClass("subtab-hidden");
} else {
$(this).addClass("subtab-hidden");
}
});
}
}
});
});
wrapper.find("ul.form-tabs > li.nav-item:not(.sub-tab) button.nav-link, ul.form-tabs > li.nav-item:not(.sub-tab) a.nav-link").each(function() {
observer.observe(this, { attributes: true, attributeFilter: ["class"] });
});
wrapper.data("subtabs-observer", observer);
}
setupMainTabHandler();
const activeMainTab = wrapper.find("ul.form-tabs > li.nav-item.active:not(.sub-tab)").first();
const activeTabLink = getTabLink(activeMainTab);
const activeTabId = activeTabLink.attr("aria-controls");
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId !== activeTabId) {
$(this).addClass("subtab-hidden");
}
});
wrapper.attr("data-subtabs-processed", "true");
}
frappe.ui.form.on("*", {
refresh: function(frm) {
setTimeout(() => {
var _a;
const wrapper = ((_a = frm.layout) == null ? void 0 : _a.wrapper) || frm.$wrapper;
setup_custom_subtabs(wrapper, frm.doctype);
}, 500);
},
onload: function(frm) {
}
});
})();
//# sourceMappingURL=custom_subtabs.bundle.DIIPT3ZZ.js.map

File diff suppressed because one or more lines are too long