diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..0dfca45 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,783 @@ +# 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 (используются `