first commit

This commit is contained in:
Awey01 2024-12-02 13:08:47 +00:00
parent 314954dd18
commit 461d6424f0
10 changed files with 21391 additions and 5 deletions

View File

@ -0,0 +1,37 @@
import frappe
def boot_session_handler(bootinfo):
# Формируем данные для всех Doctype, где есть вкладки
all_doctypes = frappe.get_all("DocType", fields=["name"])
tab_hierarchy_data = {}
for doctype in all_doctypes:
meta = frappe.get_meta(doctype["name"])
# Проверяем, есть ли вкладки (Tab Break) в Doctype
if not any(field.fieldtype == "Tab Break" for field in meta.fields):
continue
tabs = []
subtabs = {}
for field in meta.fields:
if field.fieldtype == "Tab Break":
tab_name = field.label
js_parent = field.js_parent_subtab or None
if js_parent:
if js_parent not in subtabs:
subtabs[js_parent] = []
subtabs[js_parent].append(tab_name)
else:
tabs.append(tab_name)
# Добавляем данные для Doctype
tab_hierarchy_data[doctype["name"]] = {
"tabs": tabs,
"subtabs": subtabs
}
# Передаём данные на клиентскую сторону через bootinfo
bootinfo.tab_hierarchy = tab_hierarchy_data

View File

@ -0,0 +1,10 @@
import frappe
def execute():
# Удаляем кастомное поле 'parent' из Doctype 'DocField', если оно существует
field_name = "DocField-js_parent_subtab"
if frappe.db.exists("Custom Field", {"name": field_name}):
frappe.delete_doc("Custom Field", field_name, ignore_permissions=True)
print(f"Custom Field '{field_name}' было удалено.")
else:
print(f"Custom Field '{field_name}' не существует.")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,592 @@
{
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:27:33",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label_and_type",
"label",
"fieldtype",
"fieldname",
"precision",
"length",
"non_negative",
"hide_days",
"hide_seconds",
"reqd",
"is_virtual",
"search_index",
"column_break_18",
"options",
"sort_options",
"show_dashboard",
"link_filters",
"defaults_section",
"default",
"column_break_6",
"fetch_from",
"fetch_if_empty",
"visibility_section",
"hidden",
"bold",
"allow_in_quick_entry",
"translatable",
"print_hide",
"print_hide_if_no_value",
"report_hide",
"column_break_28",
"depends_on",
"collapsible",
"collapsible_depends_on",
"hide_border",
"list__search_settings_section",
"in_list_view",
"in_standard_filter",
"in_preview",
"column_break_35",
"in_filter",
"in_global_search",
"permissions",
"read_only",
"allow_on_submit",
"ignore_user_permissions",
"allow_bulk_edit",
"column_break_13",
"permlevel",
"ignore_xss_filter",
"constraints_section",
"unique",
"no_copy",
"set_only_once",
"remember_last_selected_value",
"column_break_38",
"mandatory_depends_on",
"read_only_depends_on",
"display",
"print_width",
"width",
"max_height",
"columns",
"column_break_22",
"description",
"documentation_url",
"oldfieldname",
"oldfieldtype"
],
"fields": [
{
"fieldname": "label_and_type",
"fieldtype": "Section Break"
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"oldfieldname": "label",
"oldfieldtype": "Data",
"print_width": "163",
"search_index": 1,
"width": "163"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1,
"sort_options": 1
},
{
"bold": 1,
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
"print_hide": 1
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"label": "Index",
"oldfieldname": "search_index",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"depends_on": "eval:!doc.is_virtual",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View",
"print_width": "70px",
"width": "70px"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In List Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible",
"length": 255
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On (JS)",
"max_height": "3rem",
"options": "JS"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "default",
"fieldtype": "Small Text",
"label": "Default",
"max_height": "3rem",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If unchecked, the value will always be re-fetched on save.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch on Save if Empty"
},
{
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Display Depends On (JS)",
"length": 255,
"max_height": "3rem",
"oldfieldname": "depends_on",
"oldfieldtype": "Data",
"options": "JS"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden",
"oldfieldname": "hidden",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "set_only_once",
"fieldtype": "Check",
"label": "Set only once"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype == \"Table\"",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Perm Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"depends_on": "eval: parent.is_submittable",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
"fieldname": "remember_last_selected_value",
"fieldtype": "Check",
"label": "Remember Last Selected Value"
},
{
"default": "0",
"description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"fieldname": "display",
"fieldtype": "Section Break",
"label": "Display"
},
{
"default": "0",
"fieldname": "in_filter",
"fieldtype": "Check",
"label": "In Filter",
"oldfieldname": "in_filter",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"label": "Print Width",
"length": 10
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"length": 10,
"oldfieldname": "width",
"oldfieldtype": "Data",
"print_width": "50px",
"width": "50px"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"fieldname": "oldfieldname",
"fieldtype": "Data",
"hidden": 1,
"oldfieldname": "oldfieldname",
"oldfieldtype": "Data"
},
{
"fieldname": "oldfieldtype",
"fieldtype": "Data",
"hidden": 1,
"oldfieldname": "oldfieldtype",
"oldfieldtype": "Data"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On (JS)",
"max_height": "3rem",
"options": "JS"
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On (JS)",
"max_height": "3rem",
"options": "JS"
},
{
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "defaults_section",
"fieldtype": "Section Break",
"label": "Defaults",
"max_height": "2rem"
},
{
"fieldname": "visibility_section",
"fieldtype": "Section Break",
"label": "Visibility"
},
{
"fieldname": "column_break_28",
"fieldtype": "Column Break"
},
{
"fieldname": "constraints_section",
"fieldtype": "Section Break",
"label": "Constraints"
},
{
"fieldname": "js_parent_subtab",
"label": "JS Parent Subtab",
"fieldtype": "Data",
"description": "Укажите имя родительской вкладки для этой вкладки."
},
{
"fieldname": "max_height",
"fieldtype": "Data",
"label": "Max Height",
"length": 10
},
{
"fieldname": "list__search_settings_section",
"fieldtype": "Section Break",
"label": "List / Search Settings"
},
{
"fieldname": "column_break_35",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Tab Break\"",
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
},
{
"depends_on": "eval:!in_list([\"Tab Break\", \"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "documentation_url",
"fieldtype": "Data",
"label": "Documentation URL",
"options": "URL"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype === 'Select'",
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
},
{
"fieldname": "link_filters",
"fieldtype": "JSON",
"label": "Link Filters"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-01 15:55:44.007917",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}

View File

@ -1,17 +1,32 @@
app_name = "custom_subtabs"
app_title = "Custom Subtabs"
app_publisher = "Jey ERP"
app_description = "Custom subtabs"
app_publisher = "Jey Soft"
app_description = "custom subtubs for frappe doctypes"
app_email = "info@jeyerp.az"
app_license = "unlicense"
app_license = "mit"
# Apps
# ------------------
# required_apps = []
# Each item in the list will be shown as an app in the apps page
# add_to_apps_screen = [
# {
# "name": "custom_subtabs",
# "logo": "/assets/custom_subtabs/logo.png",
# "title": "Custom Subtabs",
# "route": "/custom_subtabs",
# "has_permission": "custom_subtabs.api.permission.has_app_permission"
# }
# ]
# Includes in <head>
# ------------------
# include js, css files in header of desk.html
# app_include_css = "/assets/custom_subtabs/css/custom_subtabs.css"
# app_include_js = "/assets/custom_subtabs/js/custom_subtabs.js"
app_include_css = "/assets/custom_subtabs/css/custom_subtabs.css"
app_include_js = "/assets/custom_subtabs/js/custom_subtabs.js"
# include js, css files in header of web template
# web_include_css = "/assets/custom_subtabs/css/custom_subtabs.css"
@ -227,3 +242,17 @@ app_license = "unlicense"
# "Logging DocType Name": 30 # days to retain logs
# }
app_include_js = [
"/assets/custom_subtabs/js/custom_subtabs.js"
]
fixtures = [
{"doctype": "DocType", "filters": [["name", "in", ["DocField"]]]}
]
after_migrate = [
"custom_subtabs.patches.add_parent_field.execute"
]
boot_session = "custom_subtabs.custom_subtabs.boot_session_handler"

View File

@ -0,0 +1,28 @@
from frappe.custom.doctype.custom_field.custom_field import CustomField
import frappe
class CustomFieldOverride(CustomField):
def on_update(self):
super().on_update()
self.setup_nested_tabs()
def setup_nested_tabs(self):
"""Автоматически подчиняет вкладки, основываясь на поле `js_parent_subtab`."""
doctype_meta = frappe.get_meta(self.dt)
fields = doctype_meta.fields
parent_mapping = {}
# Создаем структуру родительских вкладок
for field in fields:
if field.fieldtype == "Tab Break" and field.js_parent_subtab:
parent_mapping[field.js_parent_subtab] = field.fieldname
# Устанавливаем подчинение
for field in fields:
if field.fieldtype == "Tab Break" and field.js_parent_subtab:
parent_name = field.js_parent_subtab
if parent_name in parent_mapping:
parent_fieldname = parent_mapping[parent_name]
field.parent_tab = parent_fieldname # Подчиняем вкладку
frappe.db.set_value("Custom Field", field.name, "parent_tab", parent_fieldname)

View File

@ -0,0 +1,20 @@
import frappe
def execute():
# Проверяем, существует ли такое свойство
if frappe.db.exists("Custom Field", {"fieldname": "js_parent_subtab", "dt": "DocField"}):
frappe.msgprint("Свойство 'js_parent_subtab' уже существует. Пропускаем создание.")
return
# Создаём новое свойство
frappe.get_doc({
"doctype": "Custom Field",
"dt": "DocField", # Указываем DocType, для которого добавляем
"fieldname": "js_parent_subtab",
"label": "JS Parent Subtab",
"fieldtype": "Data",
"insert_after": "reqd", # Вставляем после поля "Mandatory"
"description": "Укажите имя родительского таба"
}).insert(ignore_permissions=True)
frappe.msgprint("Поле 'js_parent_subtab' успешно добавлено в настройки вкладок.")

View File

@ -0,0 +1,14 @@
/* Убираем анимацию скрытия Bootstrap */
.tab-pane.fade:not(.show) {
opacity: 0 !important; /* Только убираем анимацию, не меняем display */
}
/* Скрываем все вкладки, кроме активной */
.tab-pane {
display: none;
}
/* Показываем активную вкладку */
.tab-pane.active {
display: block !important;
}

View File

@ -0,0 +1,161 @@
frappe.ui.form.Layout.prototype.setup_tab_events = function () {
console.log("Setting up tab events...");
setTimeout(() => {
console.log("Initializing tabs...");
const tabs = this.wrapper.find("ul.form-tabs > li.nav-item");
console.log(`Found ${tabs.length} main tabs.`);
const doctype = this.frm.doctype;
const hierarchy = frappe.boot.tab_hierarchy[doctype];
if (!hierarchy) {
console.warn(`No tab hierarchy found for Doctype: ${doctype}`);
return;
}
const subtabs = hierarchy.subtabs || {};
// Process subtabs and parent tabs
tabs.each((index, tab) => {
const tabLabel = $(tab).text().trim();
if (subtabs[tabLabel]) {
console.log(`Processing subtabs for parent tab: '${tabLabel}'`);
let subtabContainer = $(tab).find(".sub-tab-container");
if (!subtabContainer.length) {
subtabContainer = $("<ul class='sub-tab-container nav'></ul>");
$(tab).append(subtabContainer);
}
subtabs[tabLabel].forEach((subtab) => {
const subtabElement = tabs.filter((_, el) => $(el).text().trim() === subtab);
if (subtabElement.length) {
const subtabClone = subtabElement.clone();
subtabClone.addClass("sub-tab").find("a").removeAttr("href");
subtabContainer.append(subtabClone);
// Удаляем лишние классы и стили у субтаба
subtabElement.remove();
}
});
}
});
// Handle main tab clicks
$(".nav-item").on("click", function () {
const clickedTab = $(this);
const tabId = clickedTab.find("a").attr("aria-controls");
console.log(`Activating main tab: '${clickedTab.text().trim()}' (ID: ${tabId})`);
// Hide all tabs
$(".form-tab-content > .tab-pane").each(function () {
const pane = $(this);
pane.removeClass("active show").css("display", "none").addClass("tab-pane fade");
});
// Activate the clicked tab
const tabContent = document.querySelector(`#${tabId}`);
if (tabContent) {
tabContent.style.display = "block";
tabContent.classList.add("active", "show");
} else {
console.warn(`Tab content not found for ID: ${tabId}`);
}
clickedTab.addClass("active");
});
// Handle subtab clicks
$(".sub-tab").on("click", function (event) {
event.stopPropagation(); // Предотвращаем срабатывание событий родительских вкладок
const clickedSubtab = $(this);
const subtabId = clickedSubtab.find("a").attr("aria-controls");
const subtabName = clickedSubtab.text().trim();
console.log(`Activating subtab: '${subtabName}' (ID: ${subtabId})`);
// Найти имя родительской вкладки через объект иерархии
let parentTabName = null;
Object.keys(subtabs).forEach((parent) => {
if (subtabs[parent].includes(subtabName)) {
parentTabName = parent;
console.log(`Found parent tab for '${subtabName}': '${parentTabName}'`);
}
});
if (!parentTabName) {
console.warn(`Parent tab not found for subtab: '${subtabName}'`);
return;
}
// Получить ID родительской вкладки через href
const parentTabId = $(`ul.form-tabs a:contains('${parentTabName}')`).attr("aria-controls");
if (!parentTabId) {
console.warn(`Parent tab ID not found for: '${parentTabName}'`);
return;
} else {
console.log(`Parent tab ID for '${parentTabName}': ${parentTabId}`);
}
// Обработать родительскую вкладку
const parentTabContent = document.querySelector(`#${parentTabId}`);
if (parentTabContent) {
console.log(`Parent tab content found:`, parentTabContent);
// Убираем классы и стили, чтобы показать родительскую вкладку
parentTabContent.style.display = "block";
parentTabContent.classList.add("active", "show");
parentTabContent.classList.remove("tab-pane", "fade");
console.log(`Parent tab '${parentTabName}' is now visible.`);
} else {
console.warn(`Parent tab content not found for ID: ${parentTabId}`);
}
// Активировать текущую субвкладку
const subtabContent = document.querySelector(`#${subtabId}`);
if (subtabContent) {
console.log(`Subtab content found:`, subtabContent);
// Убираем классы и стили, чтобы показать субвкладку
subtabContent.style.display = "block";
subtabContent.classList.add("active", "show");
subtabContent.classList.remove("tab-pane", "fade");
console.log(`Subtab '${subtabName}' content activated.`);
} else {
console.warn(`Subtab content not found for ID: ${subtabId}`);
}
clickedSubtab.addClass("active");
// Скрыть все другие вкладки, кроме текущей субвкладки и родительской
$(".form-tab-content > .tab-pane").each(function () {
const pane = $(this);
if (pane.attr("id") !== subtabId && pane.attr("id") !== parentTabId) {
console.log(`Hiding tab content for ID: ${pane.attr("id")}`);
pane.removeClass("active show").css("display", "none").addClass("tab-pane fade");
}
});
});
// Reset subtabs when switching to another main tab
$(".nav-item").on("click", function () {
$(".sub-tab").each(function () {
const subtabId = $(this).find("a").attr("aria-controls");
const subtabPane = document.querySelector(`#${subtabId}`);
if (subtabPane) {
subtabPane.style.display = "none";
subtabPane.classList.add("tab-pane", "fade");
}
});
});
}, 500); // Задержка для загрузки DOM
};

30
custom_subtabs/utils.py Normal file
View File

@ -0,0 +1,30 @@
import frappe
def process_nested_tabs(doc, method):
"""
Обработчик для вложенных вкладок.
Автоматически связывает вкладки, если у них есть указанный `js_parent_subtab`.
"""
if not hasattr(doc, 'fields') or not isinstance(doc.fields, list):
return
# Получаем все вкладки из полей
tabs = [field for field in doc.fields if field.get("fieldtype") == "Tab"]
tab_map = {tab.get("fieldname"): tab for tab in tabs}
# Проверяем каждую вкладку на наличие `js_parent_subtab`
for tab in tabs:
parent_name = tab.get("js_parent_subtab")
if parent_name and parent_name in tab_map:
parent_tab = tab_map[parent_name]
# Убедимся, что поле `sub_tabs` существует
if not parent_tab.get("sub_tabs"):
parent_tab["sub_tabs"] = []
# Добавляем текущую вкладку как дочернюю
parent_tab["sub_tabs"].append(tab)
# Обновляем поля и сохраняем документ
doc.fields = doc.fields # Переприсваиваем для безопасности
doc.save()