# Custom Subtabs - Подробная документация по архитектуре ## Обзор приложения **Custom Subtabs** — это приложение для Frappe Framework, которое добавляет возможность создания вложенных вкладок (подвкладок) в формах DocType. Приложение позволяет организовать вкладки иерархически, когда одна вкладка может содержать другие подвкладки. ### Основные возможности: - Создание иерархии вкладок (parent-child отношения) - Автоматическое перемещение подвкладок под родительскую вкладку - Визуальное отображение подвкладок с отдельным стилем - Поддержка Frappe v15 и v16 - Динамическая загрузка конфигурации через boot session --- ## Архитектура системы ### Общая схема работы ``` ┌─────────────────────────────────────────────────────────────────┐ │ Frappe Backend │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ boot_session_handler │ │ │ │ - Сканирует все DocType │ │ │ │ - Находит Tab Break поля с js_parent_subtab │ │ │ │ - Создает tab_hierarchy структуру │ │ │ │ - Передает данные на клиент через bootinfo │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ add_parent_field.py (patch) │ │ │ │ - Модифицирует docfield.json │ │ │ │ - Добавляет поле js_parent_subtab в DocField │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ frappe.boot.tab_hierarchy ↓ ┌─────────────────────────────────────────────────────────────────┐ │ Frappe Frontend (Browser) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ custom_subtabs.js │ │ │ │ - Переопределяет setup_tab_events() │ │ │ │ - Обрабатывает form refresh события │ │ │ │ - Читает tab_hierarchy из frappe.boot │ │ │ │ - Перемещает подвкладки в контейнеры │ │ │ │ - Обрабатывает клики по вкладкам │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ custom_subtabs.css │ │ │ │ - Стилизует sub-tab-container │ │ │ │ - Определяет внешний вид подвкладок │ │ │ │ - Активное состояние подвкладок │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Компоненты приложения ### 1. Backend компоненты #### 1.1 `hooks.py` **Расположение:** `custom_subtabs/hooks.py` Основной файл конфигурации приложения: - Подключает CSS и JS файлы к desk.html - Регистрирует `boot_session_handler` для передачи данных на клиент - Регистрирует патчи для выполнения после миграции ```python app_include_css = "/assets/custom_subtabs/css/custom_subtabs.css" app_include_js = "/assets/custom_subtabs/js/custom_subtabs.js" boot_session = "custom_subtabs.custom_subtabs.boot_session_handler" after_migrate = ["custom_subtabs.patches.add_parent_field.add_field_to_docfield_json"] ``` #### 1.2 `boot_session_handler` **Расположение:** `custom_subtabs/custom_subtabs/__init__.py` **Назначение:** Подготавливает данные о структуре вкладок для всех DocType и передает их на клиент. **Алгоритм работы:** 1. Получает список всех DocType в системе 2. Для каждого DocType: - Получает метаданные через `frappe.get_meta()` - Ищет поля с `fieldtype == "Tab Break"` - Для каждой вкладки проверяет наличие `js_parent_subtab` - Строит структуру `tabs` (основные вкладки) и `subtabs` (подвкладки) 3. Формирует объект `tab_hierarchy_data` вида: ```javascript { "Customer": { "tabs": ["Details", "Settings"], "subtabs": { "Settings": ["Email Settings", "Privacy Settings"] } }, "Item": { "tabs": ["Basic", "Pricing"], "subtabs": { "Pricing": ["Discounts", "Tax"] } } } ``` 4. Передает через `bootinfo.tab_hierarchy` на клиент **Пример структуры:** ```python tab_hierarchy_data = { "Customer": { "tabs": ["Details", "Settings", "Contact"], "subtabs": { "Settings": ["Email Settings", "Privacy Settings"] } } } ``` #### 1.3 `add_parent_field.py` (Patch) **Расположение:** `custom_subtabs/patches/add_parent_field.py` **Назначение:** Модифицирует основной DocField DocType, добавляя поле `js_parent_subtab`. **Что делает:** 1. Находит файл `frappe/core/doctype/docfield/docfield.json` 2. Добавляет новое поле: ```json { "fieldname": "js_parent_subtab", "fieldtype": "Data", "label": "JS Parent Subtab", "description": "Укажите имя родительского таба" } ``` 3. Вставляет поле после поля `reqd` в определении DocField 4. Сохраняет изменения в JSON файл **Зачем это нужно:** После выполнения этого патча, в интерфейсе настройки любого DocType, при добавлении поля Tab Break, появится дополнительное поле "JS Parent Subtab", где можно указать имя родительской вкладки. #### 1.4 `custom_field.py` (Override) **Расположение:** `custom_subtabs/overrides/custom_field.py` **Статус:** Закомментирован / не используется Исходно был создан для автоматической настройки вложенных вкладок через override CustomField, но в текущей реализации не используется. #### 1.5 `utils.py` **Расположение:** `custom_subtabs/utils.py` **Статус:** Содержит функцию `process_nested_tabs`, которая не используется в текущей реализации #### 1.6 `delete_custom_field.py` **Расположение:** `custom_subtabs/delete_custom_field.py` Утилита для удаления Custom Field `DocField-js_parent_subtab` (если он был создан через Custom Field вместо модификации JSON). --- ### 2. Frontend компоненты #### 2.1 `custom_subtabs.js` **Расположение:** `custom_subtabs/public/js/custom_subtabs.js` Основной JavaScript файл, реализующий логику подвкладок на клиенте. **Структура кода:** ##### A. Переопределение `frappe.ui.form.Layout.prototype.setup_tab_events` ```javascript const original_setup_tab_events = frappe.ui.form.Layout.prototype.setup_tab_events; frappe.ui.form.Layout.prototype.setup_tab_events = function () { // Вызов оригинальной функции original_setup_tab_events.call(this); setTimeout(() => { // Логика обработки подвкладок }, 500); }; ``` **Зачем переопределяется:** Frappe вызывает `setup_tab_events()` при рендеринге формы. Мы перехватываем этот вызов, чтобы добавить свою логику после стандартной обработки вкладок. ##### B. Альтернативный подход через `frappe.ui.form.on('*')` ```javascript frappe.ui.form.on('*', { refresh: function(frm) { setTimeout(() => { setup_custom_subtabs(frm); }, 500); } }); ``` **Зачем два подхода:** - Первый перехватывает встроенную логику Frappe - Второй использует event-based подход для совместимости с v16 - Оба могут работать одновременно для максимальной совместимости ##### C. Функция `setup_custom_subtabs(frm)` **Основная логика:** 1. **Получение данных:** ```javascript const currentDoctype = frm.doctype; const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype]; const subtabs = hierarchy.subtabs || {}; ``` 2. **Поиск всех вкладок:** ```javascript const tabs = wrapper.find("ul.form-tabs > li.nav-item"); ``` 3. **Обработка каждой родительской вкладки:** ```javascript tabs.each((index, tab) => { const tabLabel = $(tab).text().trim(); if (subtabs[tabLabel]) { // Это родительская вкладка с подвкладками } }); ``` 4. **Создание контейнера для подвкладок:** ```javascript subtabContainer = $("
"); sectionContainer.append(subtabContainer); ``` 5. **Перемещение подвкладок:** ```javascript subtabs[tabLabel].forEach((subtab) => { const subtabElement = tabs.filter((_, el) => $(el).text().trim() === subtab); if (subtabElement.length) { const subtabClone = subtabElement.clone(); subtabClone.addClass("sub-tab"); subtabContainer.append(subtabClone); subtabElement.remove(); // Удаляем из основного nav } }); ``` ##### D. Helper функции **`getTabLink(tabElement)`** ```javascript function getTabLink(tabElement) { const $tab = $(tabElement); let link = $tab.find("button.nav-link"); if (!link.length) { link = $tab.find("a.nav-link"); } return link; } ``` **Назначение:** Поддержка v15 (используются ``) и v16 (используются `