784 lines
32 KiB
Markdown
784 lines
32 KiB
Markdown
# 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 = $("<div class='sub-tab-container nav' data-parent-id='" + tabContentId + "'></div>");
|
||
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 (используются `<a>`) и v16 (используются `<button>`).
|
||
|
||
**`hideAllTabContents()`**
|
||
```javascript
|
||
function hideAllTabContents() {
|
||
$(".form-tab-content > .tab-pane").each(function () {
|
||
$(this).removeClass("active show").css("display", "none");
|
||
});
|
||
}
|
||
```
|
||
**Назначение:** Скрывает все содержимое вкладок перед показом выбранной.
|
||
|
||
**`findParentAndShowSubtabs(currentTabName)`**
|
||
```javascript
|
||
function findParentAndShowSubtabs(currentTabName) {
|
||
// 1. Найти родительскую вкладку для currentTabName
|
||
// 2. Показать все подвкладки этой родительской вкладки
|
||
// 3. Показать содержимое родительской вкладки
|
||
// 4. Рекурсивно обработать родительскую вкладку (если она сама подвкладка)
|
||
}
|
||
```
|
||
**Назначение:** Рекурсивная функция для обработки многоуровневых вложенностей (sub-subtabs).
|
||
|
||
##### E. Обработчики событий
|
||
|
||
**Клик по основной вкладке:**
|
||
```javascript
|
||
wrapper.off("click", ".nav-item").on("click", ".nav-item", function () {
|
||
const clickedTab = $(this);
|
||
const tabId = getTabLink(clickedTab).attr("aria-controls");
|
||
|
||
// 1. Скрыть всё содержимое
|
||
hideAllTabContents();
|
||
|
||
// 2. Показать содержимое выбранной вкладки
|
||
$(tabContent).addClass("active show").css("display", "block");
|
||
|
||
// 3. Показать/скрыть подвкладки в зависимости от родительской вкладки
|
||
$(".sub-tab-container").each(function () {
|
||
if ($(this).attr("data-parent-id") === tabId) {
|
||
$(this).find(".sub-tab").css("display", "block");
|
||
} else {
|
||
$(this).find(".sub-tab").css("display", "none");
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
**Клик по подвкладке:**
|
||
```javascript
|
||
wrapper.off("click", ".sub-tab").on("click", ".sub-tab", function (event) {
|
||
event.stopPropagation(); // Не вызывать обработчик родительской вкладки
|
||
|
||
const clickedSubtab = $(this);
|
||
const subtabId = getTabLink(clickedSubtab).attr("aria-controls");
|
||
const subtabName = clickedSubtab.text().trim();
|
||
|
||
// 1. Убрать active с других подвкладок
|
||
$(".sub-tab").removeClass("active");
|
||
clickedSubtab.addClass("active");
|
||
|
||
// 2. Скрыть всё содержимое
|
||
hideAllTabContents();
|
||
|
||
// 3. Показать содержимое выбранной подвкладки
|
||
$(subtabContent).addClass("active show").css("display", "block");
|
||
|
||
// 4. Показать родительскую цепочку
|
||
findParentAndShowSubtabs(subtabName);
|
||
});
|
||
```
|
||
|
||
#### 2.2 `custom_subtabs.css`
|
||
**Расположение:** `custom_subtabs/public/css/custom_subtabs.css`
|
||
|
||
Стили для визуального оформления подвкладок.
|
||
|
||
**Структура стилей:**
|
||
|
||
```css
|
||
.sub-tab-container {
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
display: flex !important;
|
||
justify-content: flex-start !important;
|
||
border-bottom: 1px solid #ddd !important;
|
||
}
|
||
```
|
||
**Назначение:** Контейнер для подвкладок, отображается как flexbox для горизонтального расположения.
|
||
|
||
```css
|
||
.sub-tab-container .sub-tab {
|
||
margin: 0 !important;
|
||
background: none !important;
|
||
border: none !important;
|
||
padding: 0 !important;
|
||
}
|
||
```
|
||
**Назначение:** Сброс стилей для элементов подвкладок.
|
||
|
||
```css
|
||
.sub-tab-container .sub-tab a,
|
||
.sub-tab-container .sub-tab button {
|
||
text-decoration: none !important;
|
||
display: block !important;
|
||
padding: 10px 15px !important;
|
||
color: #555 !important;
|
||
border: none !important;
|
||
font-size: 14px !important;
|
||
font-weight: normal !important;
|
||
}
|
||
```
|
||
**Назначение:** Стилизация ссылок и кнопок внутри подвкладок (поддержка v15/v16).
|
||
|
||
```css
|
||
.sub-tab-container .sub-tab.active a,
|
||
.sub-tab-container .sub-tab.active button {
|
||
color: #000 !important;
|
||
font-weight: 600 !important;
|
||
border-bottom: 1px solid #000 !important;
|
||
background-color: transparent !important;
|
||
}
|
||
```
|
||
**Назначение:** Стилизация активной подвкладки (черный текст, жирный шрифт, черная линия снизу).
|
||
|
||
---
|
||
|
||
## Как работают подвкладки (Subtabs)
|
||
|
||
### Пошаговый процесс
|
||
|
||
#### Шаг 1: Настройка в DocType
|
||
|
||
1. Администратор открывает DocType через "Customize Form"
|
||
2. Добавляет поле Tab Break (например, "Settings")
|
||
3. Добавляет еще одно поле Tab Break (например, "Email Settings")
|
||
4. В поле "Email Settings" заполняет `JS Parent Subtab` значением "Settings"
|
||
|
||
**Результат:** Вкладка "Email Settings" станет подвкладкой для "Settings".
|
||
|
||
#### Шаг 2: Backend обработка (boot session)
|
||
|
||
При загрузке страницы выполняется `boot_session_handler`:
|
||
|
||
```python
|
||
# Для Customer DocType
|
||
meta = frappe.get_meta("Customer")
|
||
for field in meta.fields:
|
||
if field.fieldtype == "Tab Break":
|
||
tab_name = field.label # "Email Settings"
|
||
js_parent = field.js_parent_subtab # "Settings"
|
||
|
||
if js_parent:
|
||
subtabs["Settings"].append("Email Settings")
|
||
```
|
||
|
||
**Результат:** Создается структура `tab_hierarchy`:
|
||
```python
|
||
{
|
||
"Customer": {
|
||
"tabs": ["Details", "Settings", "Contact"],
|
||
"subtabs": {
|
||
"Settings": ["Email Settings", "Privacy Settings"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Шаг 3: Передача данных на клиент
|
||
|
||
```python
|
||
bootinfo.tab_hierarchy = tab_hierarchy_data
|
||
```
|
||
|
||
**Результат:** На клиенте доступен `frappe.boot.tab_hierarchy`.
|
||
|
||
#### Шаг 4: Frontend обработка (отрисовка формы)
|
||
|
||
Когда пользователь открывает форму Customer:
|
||
|
||
1. **Frappe рендерит все вкладки как обычно:**
|
||
```html
|
||
<ul class="nav form-tabs">
|
||
<li class="nav-item">Details</li>
|
||
<li class="nav-item">Settings</li>
|
||
<li class="nav-item">Email Settings</li> <!-- Будет перемещена -->
|
||
<li class="nav-item">Privacy Settings</li> <!-- Будет перемещена -->
|
||
<li class="nav-item">Contact</li>
|
||
</ul>
|
||
```
|
||
|
||
2. **Вызывается `setup_custom_subtabs()`:**
|
||
- Читает `frappe.boot.tab_hierarchy["Customer"]`
|
||
- Находит `subtabs["Settings"] = ["Email Settings", "Privacy Settings"]`
|
||
- Находит в DOM элементы с текстом "Email Settings" и "Privacy Settings"
|
||
- Создает контейнер `.sub-tab-container` внутри контента вкладки "Settings"
|
||
- Клонирует элементы подвкладок в контейнер
|
||
- Удаляет оригинальные элементы из основного nav
|
||
|
||
3. **Результат HTML:**
|
||
```html
|
||
<ul class="nav form-tabs">
|
||
<li class="nav-item">Details</li>
|
||
<li class="nav-item">Settings</li>
|
||
<li class="nav-item">Contact</li>
|
||
</ul>
|
||
|
||
<div class="tab-pane" id="settings-tab-content">
|
||
<div class="sub-tab-container">
|
||
<li class="nav-item sub-tab">Email Settings</li>
|
||
<li class="nav-item sub-tab">Privacy Settings</li>
|
||
</div>
|
||
<!-- Содержимое вкладки Settings -->
|
||
</div>
|
||
```
|
||
|
||
#### Шаг 5: Взаимодействие пользователя
|
||
|
||
**Сценарий 1: Клик по основной вкладке "Settings"**
|
||
1. Скрываются все `.tab-pane`
|
||
2. Показывается `#settings-tab-content`
|
||
3. Показываются все `.sub-tab` внутри `.sub-tab-container[data-parent-id="settings-tab-content"]`
|
||
|
||
**Сценарий 2: Клик по подвкладке "Email Settings"**
|
||
1. Скрываются все `.tab-pane`
|
||
2. Показывается `#email-settings-tab-content`
|
||
3. Добавляется класс `active` к подвкладке "Email Settings"
|
||
4. Вызывается `findParentAndShowSubtabs("Email Settings")` для показа родительской цепочки
|
||
|
||
---
|
||
|
||
## Поддержка многоуровневых вложенностей
|
||
|
||
Приложение поддерживает sub-subtabs через рекурсивную функцию `findParentAndShowSubtabs`.
|
||
|
||
**Пример структуры:**
|
||
```
|
||
Settings (основная вкладка)
|
||
├── Email Settings (подвкладка)
|
||
│ ├── SMTP Settings (под-подвкладка)
|
||
│ └── IMAP Settings (под-подвкладка)
|
||
└── Privacy Settings (подвкладка)
|
||
```
|
||
|
||
**Конфигурация:**
|
||
```javascript
|
||
{
|
||
"subtabs": {
|
||
"Settings": ["Email Settings", "Privacy Settings"],
|
||
"Email Settings": ["SMTP Settings", "IMAP Settings"]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Как работает:**
|
||
1. Пользователь кликает на "SMTP Settings"
|
||
2. `findParentAndShowSubtabs("SMTP Settings")` находит родителя "Email Settings"
|
||
3. Показывает все подвкладки "Email Settings" (включая "SMTP Settings")
|
||
4. Рекурсивно вызывается для "Email Settings"
|
||
5. Находит родителя "Settings"
|
||
6. Показывает все подвкладки "Settings" (включая "Email Settings")
|
||
|
||
**Результат:** Пользователь видит всю цепочку: Settings > Email Settings > SMTP Settings
|
||
|
||
---
|
||
|
||
## Совместимость с версиями Frappe
|
||
|
||
### Frappe v15
|
||
- Использует `<a class="nav-link">` для вкладок
|
||
- Функция `getTabLink()` находит `<a>` элемент
|
||
|
||
### Frappe v16
|
||
- Использует `<button class="nav-link">` для вкладок
|
||
- Функция `getTabLink()` находит `<button>` элемент
|
||
|
||
**Универсальная реализация:**
|
||
```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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Ключевые файлы и их назначение
|
||
|
||
| Файл | Назначение | Когда выполняется |
|
||
|------|-----------|-------------------|
|
||
| `hooks.py` | Конфигурация приложения | При загрузке Frappe |
|
||
| `boot_session_handler` | Создание tab_hierarchy | При каждом входе пользователя |
|
||
| `add_parent_field.py` | Добавление поля js_parent_subtab | После миграции (once) |
|
||
| `custom_subtabs.js` | Логика подвкладок на клиенте | При загрузке каждой страницы |
|
||
| `custom_subtabs.css` | Стили подвкладок | При загрузке каждой страницы |
|
||
| `delete_custom_field.py` | Очистка Custom Field | Вручную (если нужно) |
|
||
|
||
---
|
||
|
||
## Настройка и использование
|
||
|
||
### Установка
|
||
|
||
1. **Установить приложение:**
|
||
```bash
|
||
bench get-app https://github.com/your-repo/custom_subtabs
|
||
bench --site site_name install-app custom_subtabs
|
||
```
|
||
|
||
2. **Запустить миграцию:**
|
||
```bash
|
||
bench --site site_name migrate
|
||
```
|
||
Это выполнит `add_parent_field.py` и добавит поле `js_parent_subtab` в DocField.
|
||
|
||
3. **Перезапустить bench:**
|
||
```bash
|
||
bench restart
|
||
```
|
||
|
||
### Использование
|
||
|
||
1. **Открыть DocType для настройки:**
|
||
- Перейти в "Customize Form"
|
||
- Выбрать нужный DocType (например, Customer)
|
||
|
||
2. **Настроить вкладки:**
|
||
- Добавить Tab Break с именем "Settings"
|
||
- Добавить Tab Break с именем "Email Settings"
|
||
- В поле "Email Settings" указать "JS Parent Subtab" = "Settings"
|
||
|
||
3. **Сохранить и обновить:**
|
||
- Сохранить изменения
|
||
- Обновить страницу
|
||
- Открыть любую форму этого DocType
|
||
|
||
4. **Результат:**
|
||
- Вкладка "Email Settings" появится под "Settings" как подвкладка
|
||
- При клике на "Settings" покажутся все подвкладки
|
||
|
||
---
|
||
|
||
## Технические детали
|
||
|
||
### Идентификация элементов
|
||
|
||
**Tab content ID формируется Frappe:**
|
||
```javascript
|
||
const tabContentId = tabLink.attr("aria-controls");
|
||
// Пример: "customer-settings_tab"
|
||
```
|
||
|
||
**Связь между nav-item и tab-pane:**
|
||
```html
|
||
<li class="nav-item">
|
||
<button class="nav-link" aria-controls="customer-settings_tab">Settings</button>
|
||
</li>
|
||
|
||
<div class="tab-pane" id="customer-settings_tab">
|
||
<!-- Content -->
|
||
</div>
|
||
```
|
||
|
||
### Атрибут data-parent-id
|
||
|
||
Контейнер подвкладок хранит ID родительской вкладки:
|
||
```html
|
||
<div class="sub-tab-container" data-parent-id="customer-settings_tab">
|
||
<li class="nav-item sub-tab" data-parent-id="customer-settings_tab">Email Settings</li>
|
||
</div>
|
||
```
|
||
|
||
**Использование:**
|
||
Когда пользователь кликает на основную вкладку, скрипт ищет все `.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(`<i class="fa ${iconMapping[subtab]}"></i>`);
|
||
```
|
||
|
||
---
|
||
|
||
## Отладка
|
||
|
||
### Включение логов
|
||
|
||
Скрипт содержит множество `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
|