# 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 (используются `
``` ### Атрибут data-parent-id Контейнер подвкладок хранит ID родительской вкладки: ```html
``` **Использование:** Когда пользователь кликает на основную вкладку, скрипт ищет все `.sub-tab-container` с соответствующим `data-parent-id` и показывает их. ### Таймауты ```javascript setTimeout(() => { setup_custom_subtabs(frm); }, 500); ``` **Зачем:** Frappe рендерит вкладки асинхронно. Таймаут дает время для завершения рендеринга перед обработкой подвкладок. ### Event delegation ```javascript wrapper.off("click", ".sub-tab").on("click", ".sub-tab", function () { ... }); ``` **Зачем `off()` перед `on()`:** Предотвращает дублирование обработчиков при повторном вызове функции (например, при refresh формы). --- ## Ограничения и известные проблемы ### 1. Имена вкладок должны быть уникальными Скрипт использует `text().trim()` для поиска вкладок. Если две вкладки имеют одинаковое имя, возможны конфликты. ### 2. Производительность при большом количестве DocType `boot_session_handler` сканирует все DocType при каждом входе пользователя. При большом количестве DocType (>1000) это может замедлить загрузку. **Решение:** Добавить кэширование или фильтрацию только нужных DocType. ### 3. Язык интерфейса Имена вкладок должны совпадать точно, включая локализацию. Если вкладка переведена, нужно использовать переведенное имя в `js_parent_subtab`. ### 4. Динамическое добавление вкладок Если вкладки добавляются динамически через JS после загрузки формы, подвкладки могут не обработаться. **Решение:** Вызвать `setup_custom_subtabs(frm)` вручную после добавления вкладок. --- ## Расширение функциональности ### Добавление новых стилей Отредактировать `custom_subtabs/public/css/custom_subtabs.css`: ```css .sub-tab-container .sub-tab.active button { color: #ff5733 !important; /* Красный цвет для активной подвкладки */ } ``` ### Поддержка вертикальных вкладок Изменить `.sub-tab-container`: ```css .sub-tab-container { flex-direction: column !important; } ``` ### Иконки для подвкладок Добавить логику в JS: ```javascript const iconMapping = { "Email Settings": "fa-envelope", "Privacy Settings": "fa-lock" }; subtabClone.prepend(``); ``` --- ## Отладка ### Включение логов Скрипт содержит множество `console.log()` для отладки: ```javascript console.log("Hierarchy for doctype:", hierarchy); console.log("Processing parent tab:", tabLabel); console.log("Looking for subtab:", subtab, "found:", subtabElement.length); ``` **Открыть консоль браузера (F12)** и проверить логи при загрузке формы. ### Проверка tab_hierarchy В консоли браузера: ```javascript console.log(frappe.boot.tab_hierarchy); ``` **Ожидаемый результат:** ```javascript { "Customer": { "tabs": ["Details", "Settings"], "subtabs": { "Settings": ["Email Settings", "Privacy Settings"] } } } ``` ### Проверка DOM структуры ```javascript // Проверить все контейнеры подвкладок $(".sub-tab-container").each(function() { console.log("Parent ID:", $(this).attr("data-parent-id")); console.log("Subtabs count:", $(this).find(".sub-tab").length); }); ``` --- ## Заключение Custom Subtabs — это полноценное приложение для организации иерархических вкладок в Frappe. Оно использует: - **Backend:** Python для генерации конфигурации и модификации DocField - **Frontend:** JavaScript для динамической перестройки DOM и обработки событий - **CSS:** Для стилизации подвкладок Приложение полностью интегрируется с Frappe Framework и поддерживает версии v15 и v16. --- ## Контакты - **Автор:** Jey Soft - **Email:** info@jeyerp.az - **Лицензия:** MIT