# Паттерны Smart-автоматизации

Справочник паттернов автоматизации платформы: smart-выражения, фильтры, события и триггеры, действия, пакеты, кнопки, расписания. Охватывает T-SQL, Lua, JavaScript-вставки и рецепты смарт-кнопок с динамическим URL. Для администраторов и разработчиков, настраивающих автоматизацию категорий.

## Smart-выражения — основы

**Два режима редактора.** Редактор смарт-выражения работает в двух режимах:

- **SMART** (по умолчанию) — графический конструктор с деревом сущностей
- **T-SQL** — прямой ввод SQL-запросов

**Переключение в T-SQL необратимо** — вернуться в режим SMART после конвертации невозможно.

**Дерево сущностей.** Содержит все доступные объекты для smart-выражения:

| Узел | Что содержит |
|------|-------------|
| Системные параметры | № задачи, ID заказчика, текст, дата создания, срок |
| ДП категории | Все ДП, настроенные в текущей категории |
| Свойства контекста | Зависят от точки вызова (событие, фильтр, подпись) |
| Результаты предыдущих действий | Доступны внутри пакета |
| Прочее | Все объекты системы вне контекста: категории, пользователи, группы |

Единичные объекты (например, заказчик) раскрываются стрелкой `>` для доступа к вложенным параметрам. Множественные объекты (ДП «Выбор пользователей», «Таблица») представлены как коллекции со специальными конструкциями.

**Правила составления.** При составлении смарт-выражений соблюдайте правила:

- Строковые выражения заключаются в одинарные кавычки
- Для преобразования числа в строку — функция `ВСтроку`
- Вместо ручного ввода ID — кнопка «ID объекта» (вставляет типизированную ссылку)
- Не копировать выражения из других задач/уроков — выбирать элементы вручную из дерева
- Тестировать на вкладке «Быстрое тестирование»: поле «Номер задачи» + кнопка «Тест»
- Новое выражение, которое тестируется без номера задачи и без параметров события, сначала сохранить: до сохранения «Тест» выводит сообщение «Не удалось сформировать тестовый сценарий: у смарт-выражения не задан идентификатор. Сохраните выражение перед запуском.» С указанным номером задачи тестируется текущая формула из редактора, сохранение для этого не требуется

---

## Smart-фильтры

**Базовый принцип.** Smart-фильтр — это smart-выражение, возвращающее:

- **boolean** (true/false, 1/0) — для фильтрации отдельных задач
- **массив элементов** — для отбора списка задач (в расписаниях на T-SQL)

**Фильтрация в табличных представлениях (smart-поиск).** Путь: Настройки категории > Смарты > Доступен в смарт-поиске > + Создать.

Пример — отбор задач в статусе «Новая заявка»:
```
ID статуса = <ID нужного статуса>
```

Галка «Доступен в smart-поиске» управляет видимостью отбора в интерфейсе.

![Настройки смарт-фильтра: название, переключатель «Доступен в смарт поиск» и модуль](https://help.1forma.ru/help-images/smart-actions/smart_filter_settings.png)

**Фильтрация в БИ (Блок Используется).** БИ > Добавить таблицу > Источник: smart-фильтр. Фильтр возвращает true для задач, которые должны отображаться.

Пример — задачи не в статусе «Отклонено», где ДП совпадает с текущей задачей:
```
ID категории = 33 И ID статуса != 16 И Задача.ДП[90] = Текущая задача.ДП[90]
```

**Ограничение:** если отбор задач в БИ ограничивается smart-фильтром, связи по ДП Lookup не учитываются.

**Фильтрация видимости кнопки перехода.** Переход > Ограничить доступ > smart-фильтр видимости кнопки. Фильтр возвращает true — кнопка видна, false — скрыта.

Пример — кнопка видна, если задача в ДП Lookup находится в определенном статусе:
```
Задача.ДП[«Заявка на приём»].ID статуса = <ID статуса «Организация рабочего места»>
```

**Фильтрация значений в ДП Lookup.** Настройки ДП > smart-фильтр (блок «Табличный вид»). Фильтрует задачи, доступные для выбора.

**Важно:** smart-фильтр в ДП Lookup распространяется на этот ДП во всех категориях, где он добавлен. Для ограничения по категории — добавить дополнительное условие в фильтр.

---

## Smart-события и триггеры

**Основные события.** Самые часто используемые события и их параметры:

| Событие | Параметры | Применение |
|---------|-----------|-----------|
| После постановки | Инициатор | Заполнить ДП, назначить исполнителей |
| После перехода | Переход (опционально) | Автоматические действия при смене статуса |
| После смены ДП | ДП, инициатор, новое/старое значение | Пересчет зависимых полей |
| Перед сменой ДП | ДП, инициатор, новое/старое значение, причина | Валидация, блокировка |

Если для события «После перехода» не выбран конкретный переход — пакет выполняется после любого перехода.

**Паттерн: расчет даты по условию.** Событие: после смены ДП «Дата выхода на работу».

**Кейс 1 — вычисление:** если значение не пустое, рассчитать дату окончания испытательного срока:
```
Дата выхода + Период испытательного срока (месяцев)
```
Smart-фильтр: `ДП[«Дата выхода на работу»] есть значение`

**Кейс 2 — очистка:** если значение пустое, очистить зависимое поле.
Smart-фильтр: `ДП[«Дата выхода на работу»] нет значения`

**Паттерн: автоматический переход по условию.** Событие: после перехода «Выполняется - Ожидание выдачи».
Smart-фильтр (два условия через И):
```
ДП[«Заявка на приём сотрудника»] есть значение
  И
Текущая дата >= ДП[«Дата выхода на работу»]
```
Действие: выполнить переход «Ожидание выдачи - Завершена».

---

## Smart-действия — типы

**Действие: изменить срок.** Параметры:

- **Инициатор** — по умолчанию системный пользователь
- **Срок** — значение или smart-выражение (рекомендуется Smart/TSQL)
- **Причина** — обязательна
- **Задача** — по умолчанию текущая
- **Не писать комментарий** — опционально

Пример выражения для срока:
```
ДобавитьРабочихДней(ТекущиеДатаИВремя, 3)
```

**Действие: изменить значение ДП.** Указывается целевой ДП и новое значение (фиксированное или через smart-выражение). Для очистки значения — передать пустое значение.

**Действие: назначить исполнителей.** Два действия часто используются вместе:

1. «Удалить текущих исполнителей»
2. «Добавить группу исполнителей» (параметр «Кого (группа)»)

**Порядок важен:** сначала удаление, потом добавление.

**Ограничение на количество исполнителей.** Смарт-действия «Добавить исполнителя» и «Добавить группу исполнителей» подчиняются лимиту на итоговое число исполнителей задачи (настройка `SmartActionMaxPerformers`; `0` отключает проверку). Ручное назначение исполнителей лимит не затрагивает. Подробнее: [Кастомные настройки → SmartActionMaxPerformers](https://help.1forma.ru/domains/system/settings-custom.md#smartactionmaxperformers)

**Действие: создать задачу.** Параметры: категория, заказчик, ДП при создании, связь с текущей задачей (подзадача или связанная).

**Действие: запрос подписи.** Условие запроса через smart-фильтр. Акцептанты — по smart-выражению.

Для проверки неуволенных пользователей в ДП «Выбор пользователей»:
```
в Согласующие есть элемент, такой что Уволен есть ложь
```

Коллекция `ExtParamSelectUsersValues` содержит значения ДП типа «Выбор пользователей».

---

## Строки ДП «Таблица» из SQL

Задача: автоматизацией заполнить табличный ДП строками из выборки, а по другому условию — убрать ненужные. Собирать строку-команду вручную не нужно — есть три отдельных действия, которые читают результат SQL.

1. В пакете действий на нужном событии добавьте действие **«Добавить строки ДП Таблица»**. Параметр **«ДП»** — нужный табличный параметр, **«SQL-скрипт»** — выборка, каждая колонка которой названа по идентификатору колонки таблицы:

```sql
SELECT
    l.Description AS [c1234],   -- 1234 — Id колонки «Наименование»
    l.Quantity    AS [c1235]    -- 1235 — Id колонки «Количество»
FROM custom.OrderLines l
WHERE l.OrderID = 42
```

2. Идентификаторы колонок берутся из настроек табличного ДП в категории, а не из порядкового номера столбца. Колонка выборки без имени вида `c{номер}` не переносится; если таких колонок нет ни одной, а строки есть — действие останавливается с ошибкой, чтобы не завести пустые строки.
3. Чтобы обновить существующие строки, используйте **«Изменить строки ДП Таблица»**: первая колонка выборки — идентификатор строки таблицы (берётся из таблицы `ExtParamTableValues`), остальные — `c{Id колонки}`. Строки сохраняются одним вызовом: применяются либо все, либо ни одна.
4. Чтобы удалить строки по условию, используйте **«Удалить строки ДП Таблица»** с выборкой из одного столбца — идентификатора строки. Несуществующий идентификатор для этого действия ошибкой не считается.
5. Действия можно комбинировать в одном пакете: «Изменить строки» пишет значения, «Удалить строки» убирает лишние — оба берут идентификатор строки из одной и той же таблицы `ExtParamTableValues`.

**Ограничения.** Вычисляемые колонки, а также колонки-шаблоны и колонки-кнопки этими действиями не заполняются и не изменяются. Пустой результат выборки — не ошибка, действие ничего не делает. Имена вида `c5` и `c05` указывают на одну и ту же колонку 5 — это ошибка настройки, а не две разные колонки. Параметров у скрипта нет, подстановка `@имя` не поддерживается. Действия «Добавить строки» и «Удалить строки» недоступны в 1F Certificate Edition.

## Smart-пакеты

**Последовательность.** Действия внутри пакета выполняются строго по порядку (сверху вниз). Пакеты внутри события — тоже по порядку.

Можно менять очередность перетаскиванием строк.

**Циклические пакеты.** Для повторения действий над несколькими объектами. Настройка:

1. Переключить пакет в режим «Циклический»
2. Задать **список итерируемых объектов** — smart-выражение, возвращающее массив ID
3. Опционально — **условие прерывания цикла**
4. В действиях использовать переменные цикла из контекста SMART

**Индекс итерации** начинается с 0. Доступен в smart-выражениях внутри пакета.

**Важно:** если при создании действий в циклическом пакете переменные цикла не появляются в дереве — сохранить пакет, закрыть и открыть заново.

---

## Smart-кнопки

**Создание.** Путь: Настройки категории > Кнопки > Создать. Указать имя, видимость, настройки доступа.

Отличие от кнопки перехода: smart-кнопка **не связана с маршрутом**. Примеры: рассчитать ДП, создать задачу, отправить email.

**Видимость.** Регулируется по:

- Статусу задачи
- Группам пользователя
- Smart-фильтру

**Привязка действий.** К кнопке привязывается пакет действий (+ рядом с полем «Пакеты действий»). Несколько пакетов выполняются последовательно.

Пример: кнопка «Отменить приём сотрудника» — пакет 1 меняет статус текущей задачи на «Отклонено», событие смены статуса запускает циклический пакет 2, который переводит все связанные задачи.

---

## Smart-расписания

**Настройка.** Путь: Настройки категории > Смарты > Smart-расписания > + Создать.

Параметры:

- **Периодичность** — от минут до месяцев
- **Дата и время начала** — должны быть в будущем
- **Продолжительность** — по умолчанию «10 повторений» (обычно менять на «Без ограничений»)
- **Попытки выполнения** — при ошибках, по умолчанию 5, минимум 1
- **Флажок «Активно»** — вкл/выкл

**Режимы выполнения.** Доступные режимы выполнения:

| Режим | Описание |
|-------|----------|
| Вне контекста задачи | Пакет выполняется для всех задач категории |
| Для каждой задачи, отфильтрованной | Smart-фильтр отбирает задачи |

Пример фильтра — задачи с датой выхода на работу = сегодня:
```
УсечьВремя(ДП[«Дата выхода на работу»]) = УсечьВремя(ТекущиеДатаИВремя)
```

Функция `УсечьВремя` необходима для сравнения дат без учета времени.

**Ограничения.** Что нужно учитывать:

- Расписание работает только при наличии хотя бы одной задачи в категории
- Пакеты с действием «Отменить» недоступны в расписаниях
- Для действий без привязки к категории — глобальные smart-расписания (Общие SMART)

**Проверка.** Как проверить настройку:

- Тестирование фильтра через кнопку «Тест»
- Принудительный запуск: ПКМ на расписании > «Выполнить» (или кнопка «Запустить сейчас»)

---

## T-SQL в Smart

**Когда использовать.** T-SQL-скрипты стоит применять, когда:

- Стандартных возможностей SMART недостаточно
- Нужны JOIN-ы между категориями
- Сложные условия с подзапросами
- Фильтрация расписаний (T-SQL фильтр возвращает **список задач**, не boolean)

**Правило:** в первую очередь используйте язык SMART. T-SQL — когда SMART не справляется.

**Ключевые таблицы и переменные.** Основные таблицы и переменные, используемые в примерах:

| Объект | Описание |
|--------|----------|
| `TasksInSubcat{N}Denormalized` | Денормализованная таблица категории N |
| `ExtParam{N}NativeValue` | Значение ДП N (нативный тип) |
| `ExtParam{N}Value` | Значение ДП N (строка) |
| `@ContextID` | ID текущей задачи |
| `@eventParam0` | ID задачи в контексте подписи |
| `ExtParamSelectUsersValues` | Таблица значений ДП «Выбор пользователей» |
| `Users` | Таблица пользователей (IsFired_2 = признак увольнения) |
| `TaskHelpers` | Исполнители задачи (IsResponsible = ответственный) |
| `dbo.tc_AddWorkingDays(date, days)` | Стандартная функция: добавить рабочие дни |
| `IsWaitingSign` | Признак «на подписи» (1/0) |

**Паттерн: условный расчет срока.** Рассчитать срок по условию:

```sql
SELECT
   CASE
       WHEN Extparam84NativeValue IS NOT NULL
            AND Extparam71NativeValue > GETDATE()
       THEN dbo.tc_AddWorkingDays(Extparam71NativeValue, 1)
       ELSE dbo.tc_AddWorkingDays(GETDATE(), 1)
   END
FROM TasksInSubcat21Denormalized
WHERE Taskid = @ContextID
```

**Паттерн: фильтр расписания — отбор задач.** Отобрать задачи для расписания:

```sql
SELECT t29.Taskid
FROM TasksInSubcat29Denormalized t29
JOIN TasksInSubcat18Denormalized t18
    ON t18.taskid = t29.Extparam114NativeValue
WHERE t29.Stateid = 28
    AND t18.Extparam277Value <> ''
    AND FORMAT(t29.Extparam74NativeValue,'dd.MM.yyyy')
        <= FORMAT(GETDATE(),'dd.MM.yyyy')
    AND t29.IsWaitingSign = 0
```

**Паттерн: проверка акцептантов подписи через SQL.** Проверить акцептантов подписи запросом:

```sql
if exists (
    select u.UserID
    from TasksInSubcat22Denormalized t22 (nolock)
    join TasksInSubcat19Denormalized t19 (nolock)
        on t19.TaskID = t22.ExtParam88NativeValue
    join ExtParamSelectUsersValues epvu (nolock)
        on t19.TaskID = epvu.TaskID
        and epvu.ExtParamID = 76
    join Users u (nolock)
        on u.UserID = epvu.UserID
    where t22.TaskID = @ContextId
        and u.IsFired_2 = 0
    )
  select 1
else
  select 0
```

**Для акцептантов:** параметр задачи = `@eventParam0` (не `@ContextId`).

---

## Изменение настроек с учетом автоматизации

**Чек-лист при изменениях.** Что проверить при изменении настроек категории:

1. **Добавление ДП в категорию:** проверить, есть ли автоматизация, создающая задачи в эту категорию (Маршрут > Постановка задач в категорию). Если ДП обязательный — добавить его в действие «Создать задачу»
2. **Удаление ДП:** проверить все связанные выражения и пакеты. Лучше скрыть, чем удалить
3. **Удаление перехода:** если на переходе настроена автоматизация — она удалится вместе с переходом. Лучше скрыть
4. **Проверка зависимостей:** вкладка «Smart выражения» показывает, в каких пакетах используется выражение

---

## LUA-автоматизация

**Преимущества Lua перед SMART.** Преимущества Lua-скриптов перед графическим конструктором SMART:

- Снижение нагрузки на БД (скрипты выполняются вне СУБД)
- Быстрее SMART-выражений
- Обработка контекстных данных, которых еще нет в БД
- Полноценный язык: циклы, таблицы, строки
- Защита от SQL-инъекций

Для простых задач — SMART предпочтительнее (проще поддержка).

**Основные библиотеки 1Формы.** Основные библиотеки, доступные в Lua-скриптах:

| Объект | Метод | Назначение |
|--------|-------|-----------|
| `CONTEXT` | `.Id`, `.OwnerId`, `.ExtParamValues` | Контекст текущей задачи |
| `SQL` | `:query(sql, params)` | Выполнить SQL-запрос, вернуть таблицу результатов |
| `SMART` | `:execute_action(action, contextId, 'task', params)` | Вызвать смарт-действие |
| `UTILS` | `:json_encode(table)` | Сериализовать таблицу в JSON |
| `EVENTPARAMS` | `["HasChangedUser"]`, `["NewValue"]` | Параметры события |
| `SESSION_USER` | `.Id` | Текущий пользователь сессии |
| `RESULT` | (зарезервировано) | Выходной параметр скрипта |

Необязательный четвёртый аргумент `true` у `execute_action` выполняет действие в фоне. Если скрипт выполняется внутри открытой транзакции, фоновое действие стартует только после её фиксации и не выполняется, если транзакция откатится; вне транзакции действие стартует сразу.

**Паттерн: получение значения ДП из контекста.** Получить значение ДП из контекста:

```lua
local function getExtParamValue(ep_id)
    for _, value in pairs(CONTEXT.ExtParamValues) do
        if value.ExtParamId == ep_id then
            return value.DateTimeValue, value.ExtParamValue
        end
    end
    return nil, nil
end
```

Возвращает два значения: timestamp и строковое представление.

**Паттерн: SQL-запрос с параметрами.** Выполнить SQL-запрос с параметрами:

```lua
local query = [[
    SELECT t.TaskID, t.ExtParam55Value as Name
    FROM TasksInSubcat34Denormalized as t WITH (nolock)
    WHERE t.StateID = 7
    AND t.ExtParam114NativeValue = 1186
]]
local params = {}
local res = SQL:query(query, params)

for _, data in pairs(res) do
    print(data.TaskID, data.Name)
end
```

Параметры передаются как таблица `{ParamName = value}`, в SQL используются как `@ParamName`. Ключи результата регистрозависимые (как в SQL-запросе).

**Паттерн: создание задачи с заполнением ДП.** Создать задачу и заполнить её ДП:

```lua
local function createTask(owner, helpers, due_time, newtaskextparams_json)
    SMART:execute_action('CreateTask', CONTEXT.Id, 'task', {
        Owner = owner,
        Subcat = 31,               -- ID категории
        TaskText = nil,
        CreateLink = false,
        CreateSubtask = true,       -- подзадача текущей
        Performers = helpers,       -- таблица ID пользователей
        DueTime = due_time,         -- timestamp
        ExtParams = newtaskextparams_json,  -- JSON
        NewTaskCopySubscribers = true,
        Priority = 1
    })
end

-- Подготовка ДП для новой задачи
local newtaskextparams = {
    {ExtParamID = 99,  FixedValue = project_id},
    {ExtParamID = 55,  FixedValue = name},
    {ExtParamID = 102, FixedValue = description},
}
local json = UTILS:json_encode(newtaskextparams)
createTask(owner, helpers, due_time, json)
```

**Получение ID созданной задачи.** `SMART:execute_action('CreateTask', …)` возвращает **массив** идентификаторов созданных задач (C# `System.Int32[]`), а не одиночное число — даже если создана одна задача. ID первой задачи берётся по индексу `0`:

```lua
local res = SMART:execute_action('CreateTask', CONTEXT.Id, 'task', { ... })
local newTaskId = res[0]   -- или res:GetValue(0)
```

Индексация `res[1]` вернёт `nil`: это .NET-массив с нумерацией элементов **с нуля**, а не Lua-таблица (нумерация с 1).

**Результат «Распаковать».** У действия `UnzipArchive` результат устроен так же, пока в архиве не было пропущенных записей: это .NET-массив идентификаторов распакованных файлов, первый файл — `res[0]`. Если часть записей архива пропущена (небезопасный путь, некорректное имя, превышение порога по объёму), действие возвращает таблицу `{ fileIds, skippedEntries }` с нумерацией с единицы: идентификаторы лежат в `fileIds`, а имена пропущенных записей — в `skippedEntries`. Полное описание форм — в [справочнике действий](https://help.1forma.ru/domains/smart-actions/actions-reference-files-data.md), раздел «Файлы (расширение)».

### Паттерны: срок и даты

**Паттерн: обновление срока задачи.** Обновить срок задачи:

```lua
local function updateTaskOrderedTime(task_id, due_time)
    SMART:execute_action('UpdateTaskOrderedTime', task_id, 'task', {
        InitUser = EVENTPARAMS["HasChangedUser"].Id,
        DueTime = due_time,
        Reason = 'Изменение даты планового открытия проекта',
        Task = task_id,
        DoNotWriteComment = false
    })
end
```

**Важно:** при событии «После смены ДП» новое значение уже в БД. При «Перед сменой ДП» — новое значение в `EVENTPARAMS["NewValue"]`, в БД еще старое.

**Каскад правил.** Если правило на событие «После смены ДП» запускается смарт-действием другого правила — правило А изменило параметр, на его изменение сработало правило Б, — то правило Б читает из базы значение, записанное правилом А, а не прежнее: и из денормализованной таблицы категории, и из значений параметров. То же верно при сохранении пачки параметров одной операцией. На сборках ниже 2.268 надёжный обходной путь — брать параметр события «Новое значение» вместо чтения из базы; он доступен только для того параметра, сменой которого запущено правило.

**Паттерн: расчет срока от плановой даты.** Рассчитать срок от плановой даты:

```lua
local project_open_date = getExtParamValue(93)  -- timestamp

if project_open_date then
    due_time = project_open_date - work_days * 24 * 3600
end
if not due_time or due_time <= os.time() then
    due_time = os.time() + 18 * 3600  -- fallback: сегодня + 18ч
end
```

**Стиль кода Lua (рекомендации).** Рекомендации по стилю Lua-кода:

| Правило | Пример |
|---------|--------|
| Всегда `local` | `local function f()`, `local x = 1` |
| snake_case для переменных | `helpers_table`, `due_time` |
| camelCase для функций | `getExtParamValue()`, `createTask()` |
| UPPER_CASE для констант | `PI = 3.14`, `MAX_RETRY = 5` |
| `_` для неиспользуемых переменных | `for _, v in pairs(t) do` |
| Проверка на nil без явного сравнения | `if var then` вместо `if var ~= nil then` |
| Объявлять функции до вызова | Порядок: вспомогательные, затем основной код |
| Комментарии к `end` | `end -- if string`, `end -- for each t` |

Значение по умолчанию:
```lua
x = x or 'default'
```

Тернарный оператор:
```lua
answer = (2*2 == 4) and 'Истина' or 'Ложь'
```

---

## JS в карточках задач

**Назначение.** JS-вставки управляют представлением карточки задачи: скрытие/отображение ДП, подсветка полей, динамическое поведение в зависимости от условий.

Путь: Настройки категории > Формы > Настройки JS и CSS вставок (NTF).

**Доступные API.** Доступные в JS-вставках объекты и переменные:

| Переменная/объект | Описание |
|-------------------|----------|
| `taskId` | Номер текущей задачи |
| `StateID` | ID текущего статуса |
| `SessionUserId` | ID текущего пользователя |
| `getSessionUserInfo()` | `{userId, isAdmin, groups[]}` |
| `new ExtParam(id)` | Объект ДП для чтения/управления |

**Паттерн: скрытие ДП по значению другого ДП.** Скрыть поле «Телефон» если «Вид связи» = «E-mail личный»:

```javascript
// Обработчик NTF — обернуть в событие открытия задачи
var epVidSvyazi = new ExtParam(1234);  // ID ДП «Вид связи»
var epPhone = new ExtParam(1235);      // ID ДП «Телефон»
var epEmail = new ExtParam(1236);      // ID ДП «E-mail»

var val = epVidSvyazi.getValue();
if (val === 'E-mail личный') {
    epPhone.hide();
    epEmail.show();
} else if (val === 'Телефон личный') {
    epEmail.hide();
    epPhone.show();
} else {
    epPhone.hide();
    epEmail.hide();
}
```

**Паттерн: подсветка незаполненных полей.** Подсветить незаполненные поля на форме:

```javascript
var epList = [1234, 1235, 1236, 1237]; // ID обязательных ДП
epList.forEach(function(id) {
    var ep = new ExtParam(id);
    if (!ep.getValue()) {
        ep.setLabelColor('red');
    }
});
```

**Активация вставки.** Как активировать вставку:

- Создать вставку (тип: js)
- Добавить в категорию
- Поставить галку «Форма задачи» для активности
- Для NTF — обернуть код в обработчик события открытия задачи
- Для MTF — создать отдельную функцию-обработчик (для корректного удаления)

---

## Рецепты смарт-кнопок с динамическим URL

Смарт-кнопка может открывать URL, сформированный динамически на основе данных текущей задачи. Механизм — поле **URL** в настройках кнопки (тип «Произвольная ссылка»), которое принимает смарт-выражение, возвращающее строку.

Справочник переменных: [`docs/domains/smart-actions/variables-reference.md`](https://help.1forma.ru/domains/smart-actions/variables-reference.md)

---

**1. Открыть внешний URL с ID задачи.** Сценарий: кнопка «Открыть в CRM» — переходит на внешний сервис, передавая `taskId` как параметр запроса.

**Как настраивается:**

1. Настройки категории → вкладка **Кнопки** → Создать
2. Заполнить **Имя** кнопки
3. В поле **URL** выбрать тип «Произвольная ссылка»
4. Привязать смарт-выражение, возвращающее URL со встроенным ID задачи
5. **Открывать ссылку в** → «В новом окне» (для внешних ресурсов рекомендуется)

**Смарт-выражение (T-SQL):**

```sql
SELECT 'https://external.service/form?taskId=' + CAST(@ContextID AS nvarchar(20))
```

`@ContextID` — ID текущей задачи (TaskID). Подставляется автоматически в контексте смарт-выражения.

**Смарт-выражение (SMART-режим, без T-SQL):**

В режиме SMART конкатенация строк: `'https://external.service/form?taskId='` + `ВСтроку(№ задачи)`.

**Важно:** внешние ресурсы с заголовком `X-Frame-Options: sameorigin` нельзя открывать в модальном окне — браузер заблокирует. Используйте режим «В новом окне».

---

**2. mailto с динамической темой и телом.** Сценарий: кнопка «Написать письмо» — открывает почтовый клиент пользователя с заполненными получателем, темой и телом письма.

**Вариант A — через поле URL кнопки (смарт-выражение):**

```sql
SELECT 'mailto:support@example.com?subject=Задача%20' + CAST(@ContextID AS nvarchar(20))
    + '&body=Добрый%20день%2C%20прошу%20помочь%20с%20задачей%20' + CAST(@ContextID AS nvarchar(20))
```

**Вариант B — через JS-выражение кнопки:**

```javascript
window.open('mailto:support@example.com?subject=Задача ' + taskId + '&body=Текст письма');
```

Переменная `taskId` доступна в JS-вставках карточки задачи. JS-выражение указывается в поле **JavaScript выражение** настроек кнопки. Если указаны и JS, и URL — сначала выполняется JS, затем открывается URL.

**Кириллица в параметрах:** браузеры обычно кодируют её автоматически, но для надёжности — кодируйте вручную: пробел = `%20`, запятая = `%2C`.

### Продвинутые рецепты

**3. Открыть связанную задачу в новой вкладке.** Сценарий: кнопка «Перейти к проекту» — открывает карточку связанной задачи (ID берётся из ДП текущей задачи).

**Рекомендованный способ (тип URL = «Форма задачи»):**

В поле URL выбрать тип **«Форма задачи»** (RedirectActionType.OpenTask = 3). Привязать смарт-выражение, возвращающее ID нужной задачи:

```sql
-- Вернуть ID связанной задачи из ДП (ДП №77 — ID проекта)
SELECT ExtParam77NativeValue
FROM TasksInSubcat{N}Denormalized
WHERE TaskID = @ContextID
```

Система сама сформирует корректный внутренний URL `/tasks/{taskId}`.

**Альтернатива через OpenUrl (явный URL):**

```sql
SELECT '/tasks/' + CAST(ExtParam77NativeValue AS nvarchar(20))
FROM TasksInSubcat{N}Denormalized
WHERE TaskID = @ContextID
```

**Режим открытия:** «В новом окне» или «В модальном окне» — на выбор администратора.

---

**4. Вызов внешнего HTTP API без открытия браузера.** Сценарий: кнопка «Отправить в 1С» — при нажатии выполняет HTTP-запрос к внешнему API серверной стороной, не открывая URL в браузере.

**Механизм:** пакет действий со стандартным действием **«Отправить HTTP запрос»** (StandardAction.SendHttpRequest). Кнопка запускает пакет через поле **Пакет действий** — не через поле URL.

**Конфигурация пакета действий:**

| Параметр | Значение |
|----------|----------|
| Метод | GET / POST / PUT / DELETE / PATCH |
| URL | Смарт-выражение → строка с адресом |
| Параметры запроса | Список `Имя=Значение` (для GET — query string, для POST — body) |
| Заголовки | Список `Имя=Значение` (например, Authorization) |
| Raw Body | Строка (для POST с JSON — вместо параметров) |
| Таймаут, мс | Необязательный, по умолчанию стандартный |

**Пример URL в пакете (смарт-выражение):**

```sql
SELECT 'https://api.external.service/tasks/' + CAST(@ContextID AS nvarchar(20)) + '/sync'
```

**Отличие от поля URL кнопки:** поле URL кнопки открывает адрес в браузере (клиентская сторона). Действие «Отправить HTTP запрос» выполняется сервером — пользователь не видит переход, ответ можно использовать в следующих действиях пакета.

---

**Ловушки.** Частые ошибки и их решения:

| Проблема | Решение |
|----------|---------|
| Внешний URL в модальном окне — ошибка `X-Frame-Options: sameorigin` | Переключить «Открывать в» на «В новом окне» |
| Кириллица в URL отображается как `%D0%...` | Норма для HTTP; если нужна читаемость — использовать только ASCII в параметрах |
| Смарт-выражение возвращает NULL | Переход по ссылке не выполняется без сообщения. Добавьте проверку в SQL: `ISNULL(result, 'https://fallback.url')` |
| Длинный URL обрезается | Ограничений на стороне платформы не задокументировано; браузерный лимит — ~2000 символов. Длинные данные передавайте через POST-запрос в пакете |
| JS-выражение с ошибкой блокирует переход по URL | Если указаны и JS, и URL — при ошибке JS переход по URL не выполняется (проверить через консоль браузера) |
| Переменная `@ContextID` не работает в JS-выражении кнопки | `@ContextID` — только для смарт-выражений T-SQL. В JS используйте переменную `taskId` |
