# Гриды — Справочник фильтров

Справочник соответствия типов колонок грида и применяемых фильтров.

Справочник описывает, какой фильтр применяется для каждого типа колонки в гриде задач и в ДП Таблица: текстовые фильтры, фильтры по пользователям, по подзадачам и вопросам, по дополнительным параметрам, а также фильтры колонок ДП Таблица и особую обработку сквозных ДП (Through).

## Два контекста фильтрации

Каждый фильтр работает в одном или обоих контекстах:

- **Грид задач** — фильтрация списка задач категории;
- **ДП Таблица** — фильтрация строк внутри табличного дополнительного параметра.

Окно выбора задачи по ДП «Ссылка» и «Множественная ссылка» относится к первому контексту, но с оговоркой по правам: категории, задачи которых оно показывает, в параметры выборки попадают позже проверки доступа, поэтому для критерия доступа к колонкам ДП они подставляются отдельно — из описания лукапа, из контекста вызова или из категории-владельца. В саму выборку, сортировку и агрегаты этот набор не пишется. Если ни один из источников категорий не сработал, набор остаётся пустым и операции по любой колонке ДП запрещены; для сводного раздела параметр считается закрытым, если он закрыт хотя бы в одной из категорий-источников.

**Общий контракт отбора по параметрам в представлениях.** Отбор по дополнительным параметрам и основным колонкам в «Канбане», «Календаре», «Ресурсах» и «Ленте» собирается единым контрактом: по идентификаторам выбранных значений, а подпись значения хранится отдельно и нужна только для чипа над списком. У диаграммы Ганта та же кнопка фильтров, что и в канбане: список полей строится по настройкам табличного вида категории и включает как основные колонки, так и дополнительные параметры, а выбранные условия уходят в запрос данных Ганта. Табличное представление категории в этот набор не входит: оно остаётся на собственной фильтрации грида.

![Фильтр колонки в ДП Таблица: операторы «Содержит», «Начинается с», «Пусто» и другие](https://help.1forma.ru/help-images/grids/dp_table_filter.png)

## Фильтры гридов задач

### Требования к имени колонки

Имя колонки проверяется дважды, и правила у этих проверок разные.

**Проверка входа запроса.** Имена колонок сортировки и ключи фильтров, приходящие в запросе, проверяются до проверки на признаки SQL-инъекций: имя должно быть не длиннее 128 символов и не содержать апострофа, точки с запятой, `--`, `/*`, `*/`, управляющих символов и переводов строк. Пробел, кириллица, запятая, скобки, точка, слеш и дефис допустимы — под них заведены живые имена колонок. Не прошедшее имя отклоняет запрос целиком, а не очищается. Проверка одинакова для табличного представления, ленты задач, поиска по ленте, произвольных источников данных и фильтра Ганта; пустая колонка сортировки — и строка из одних пробелов — означает отсутствие сортировки и проверку не проходит; такой элемент сортировки отбрасывается, а запрос обрабатывается по остальным элементам.

**Проверка критерия отбора.** Имя колонки, по которой строится критерий отбора, проверяется перед привязкой к SQL белым списком идентификаторов: допускаются обычные и квалифицированные имена (до трёх частей через точку, в скобочной или кавычечной форме) длиной до 400 символов, одиночные внутренние дефисы разрешены — их содержат имена параметров планировщика ресурсов. Не проходят: пробелы, скобки и прочие спецсимволы внутри имени, сдвоенные и хвостовые дефисы. Последствие зависит от пути: в критерии отбора невалидное имя приводит к тому, что критерий молча отбрасывается (фильтр перестаёт влиять на выборку), на пути получения безопасного имени колонки — к ошибке «Filter … has an invalid column name for SQL criteria».

Симптом для поддержки: фильтр настроен, но выборка его игнорирует — проверить имя колонки в настройке фильтра; таблица или лента не грузится сразу после правки запроса, отбора или вида — проверить имя колонки в запросе: недопустимое имя теперь отклоняет запрос, а не очищается.

## Фильтры колонок ДП Таблица

Эти фильтры работают только в контексте ДП Таблица:

| Тип колонки | Тип фильтра | Описание |
|---|---|---|
| Lookup-колонка в таблице | `TextFilter` | Текстовый поиск по значению lookup-связи внутри ДП Таблица (по тексту связанной задачи и отображаемым полям справочника), как в гриде списка задач; не ограничен числом записей справочника. Зависит от `LookupParamSettings`. |
| Виртуальная (вычисляемая) колонка | `TextFilter` / `NumberFilter` | Текстовые операции ищут по отображаемому значению колонки — тому, что видно в таблице, а не по идентификатору связанной задачи. Может не поддерживать все операции. |
| "Выбор пользователей" в таблице | `SetFilter` | Фильтр по пользователям/группам внутри ДП Таблица. |

## Общее меню фильтров по дополнительным параметрам (канбан, календарь, ресурсы, гант, лента)

Одно и то же меню фильтров работает в представлениях категории — канбане, календаре, ресурсах, ганте и ленте (а также в табличном представлении, где оно встроено в тулбар): оно строит список доступных полей, даёт выбрать поле и операцию и превращает выбранное значение в модель запроса.

Меню открывается в модальном окне, и на всех экранах, где есть кнопка фильтра, — сводный букинг, режим задач, виджет портала, лента событий, поиск по всем задачам, канбан, динамические виджеты, — открывается одинаково. Если по кнопке фильтра экран только затемняется, а окна нет (в консоли браузера при этом ошибка NG0201), это дефект, а не настройка фильтра.

**Список доступных полей.** В меню попадают и дополнительные параметры категории, и основные поля задачи, на которые у пользователя есть права просмотра. Список строится по названию поля, состав основных полей берётся из настроек колонок табличного представления; поле доступно для фильтрации только при поддерживаемом типе, а набор операций ограничен допустимыми для этого поля.

**Ключ строки меню.** Строка меню различается составным ключом «источник — категория — поле»: один и тот же ДП в двух категориях даёт разные строки меню и отдельное состояние, поэтому выбор фильтра в одной категории не подсвечивается в другой. В модель запроса уезжает прежний ключ: `ExtParam{id}` для дополнительного параметра и имя поля для основной колонки представления.

**Смена категории.** При переключении на другую категорию без перезагрузки страницы меню очищает выбранные чипы (в том числе уже применённый фильтр) и перечитывает список доступных полей для новой категории.

**Формы запроса.** Значение фильтра превращается в одну из форм `filterModel`:

| Вид фильтра | Форма запроса |
|---|---|
| Текст | `{ filterType: 'text', type, filter }` |
| Число | `{ filterType: 'number', type, filter }` |
| Дата | `{ filterType: 'date', type, dateFrom, dateTo? }` |
| Справочник и мультисправочник | `{ filterType: 'text', type: 'equals', filter: '<подписи>', customInfo: { option: '<префикс>:<id,id>' } }` |
| Флажок | `{ filterType: 'set', values }` |

Операции без значения (`blank`, `notBlank`) уезжают формой поля; пустой фильтр в запрос не попадает. Для справочника подписи идут в `filter`, идентификаторы — в `customInfo.option` с префиксом: `any:` — достаточно совпадения с любым из выбранных значений, `all:` — в задаче должны быть все выбранные значения (мультисправочник).

## Маппинг: тип ДП → фильтр

Какой фильтр применяется для каждого типа дополнительного параметра:

| Тип ДП | Грид задач | ДП Таблица | Примечание |
|---|---|---|---|
| `String`, `MultiLineString` | `TaskTextFilter` (или стандартный) | стандартный | Текстовый |
| `Number`, `Decimal`, `Money` | стандартный | стандартный | Числовой |
| `DateTime` | `agDateColumnFilter` | `agDateColumnFilter` | Дата и время: штатный фильтр даты AG Grid — время можно указать в самом фильтре, год ограничен диапазоном 1000–9999. Подробности — ниже |
| `Boolean` | стандартный | стандартный | Чекбокс |
| `LookUpField` | `LookupFilter` | `LookupColumnFilter` | Lookup |
| `MultiSlctSubcatTasks` | `MultiSelectFilter` | стандартный | Мультилукап |
| `SelectUsersType`, `SelectGroupsType`, `SelectOrgUnit` | `SelectUsersExtParamFilters` | `SelectUsersColumnFilter` | Выбор пользователей |
| `MultiCheckbox`, `DropDown` | `MultiSelectFilter` | стандартный | Множественный выбор |
| `Table` | **Ограничено** (не через `ShowTasksFeed`) | N/A (Table — это сам грид) | Фильтрация задач возможна через HTML-представление при настройке шаблона. См. [support-guide §1.3](https://help.1forma.ru/domains/grids/support-guide.md#13-фильтр-по-дп-в-гриде) |
| `Through` | **Особая обработка** | N/A | Сквозной ДП: действующий тип сервер берёт у параметра-источника в конце цепочки и по нему готовит значение фильтра, но в хранимую процедуру передаёт тип `Through` — процедура сама определяет целевой тип и применяет правильную логику. Замена на `SelectUsersOrgUnits`/`SelectUsersGroups` происходит **только при группировке**, не при фильтрации. Колонка, которая ссылается на удалённый или недоступный параметр, фильтр не ломает: условие по ней не применяется, остальные условия работают как обычно. Если у сквозного параметра источник не определяется и колонка доступна, фильтр прерывается сообщением о неверной настройке сквозного параметра — цепочку правит администратор. Типичные применения: Through → ДП «Выбор пользователей» (Менеджер клиента, РП, BDM и др.) |
| `File` | **ИСКЛЮЧЁН** | N/A | Файловый ДП |
| `Numerator` | стандартный | стандартный | Нумератор |
| `Virtual` (колонка таблицы) | N/A | `VirtualColumnFilter` | Только внутри ДП Таблица |

**Тип фильтра как настройка колонки.** В настройках табличного вида категории тип фильтра колонки — самостоятельный параметр: в сохранённых настройках колонки может лежать явно выбранный тип, а не только выведенный из типа ДП. Допустимый набор значений зависит от типа ДП колонки (см. таблицу в [business.md](https://help.1forma.ru/domains/grids/business.md), раздел «Тип фильтра колонки»); при чтении сохранённого вида значение вне допустимого набора отбрасывается и подставляется значение по умолчанию. Тип «Двойной выбор значений» строится на тех же операторах, что и одиночный фильтр, но с двумя условиями и переключателем «И» / «ИЛИ».

**Фильтр по дате и по дате-времени.** Колонки «дата» и «дата и время» фильтруются штатным `agDateColumnFilter` AG Grid. Если время задано, операторы сравнивают дату вместе с ним; если время не указано — сравнение идёт по дате. Время вводится с точностью до минуты: секунды в пикере скрыты, поэтому условие «18:29» находит и значение 18:29:37. Оператор по умолчанию — «Равно», год в пикере ограничен диапазоном 1000–9999. Набор операторов зависит от источника данных грида: в гридах задач доступны «Равно», «Не равно», «Меньше», «Больше» и «Диапазон», а на формах — только «Равно» и «Диапазон», потому что на формах сервер отбрасывает время и остальные операторы дали бы неверную выборку. Отбор «Нет значения» и «Есть значение» доступен в обоих случаях.

**Обратный диапазон не применяется и объясняется на языке интерфейса.** Если верхняя граница диапазона раньше нижней, отбор не выполняется, а под полем фильтра выводится подсказка на языке интерфейса; дата в ней показана в привычном формате приложения. Подсказка — не часть модели фильтра: её текст и формат даты задаёт платформа (см. frontend.md, раздел «Локализация подписей грида»), а не библиотека. Набор операторов и логика выборки при этом не меняются: условия с верными границами применяются как раньше.

**День, выбранный без времени, — это сутки.** При выборе дня в календаре и по кнопке «Сегодня» в поле времени подставляется `00:00`, а не текущее время, и условие отбирает записи за весь выбранный день, а не за одну минуту. Суточную семантику задаёт клиент: условие «Равно» с полуночью он превращает в диапазон от `00:00:00` этого дня до `00:00:00` следующего, а сервер сравнивает границы ровно такими, какими их получил. Поэтому в отбор попадают сутки целиком — последняя секунда дня входит, а первые сутки следующего дня уже нет. По той же причине конечный день «Диапазона» входит целиком, а не до последней минуты суток. Сам фильтр при этом не переписывается: в сохранённом представлении и в чипе над таблицей условие остаётся «Равно» с `00:00`. Набранное вручную время сравнивается точно: условие «12:30» отбирает записи этой минуты. У колонок «только дата» поля времени нет, и фильтр по-прежнему сравнивает даты.

**Границы «Диапазона» по колонкам «дата и время».** Диапазон работает как полуинтервал: начальная граница включается, конечная — нет. Конец дня клиент переводит в полночь следующих суток, поэтому выбранный последним день попадает в отбор целиком. Если задать обе границы одинаковыми, верхняя не включается и выборка остаётся пустой. По колонкам «только дата» конечная дата входит в диапазон целиком.

**Пустое поле — это отсутствие значения.** При выводе колонки поле пустое, текущая дата в него не подставляется; пустое поле означает, что значение не задано, а не полночь текущих суток. Повторная установка той же даты после очистки применяется так же, как первая: очистка сбрасывает состояние фильтра, поэтому значение с датой и временем `00:00` отличается от пустого состояния, и отбор запускается без перезагрузки страницы.

**«Не равно» по выбранному дню.** День, выбранный в календаре без набранного времени, исключается целиком — и по колонке «только дата», и по колонке «дата и время»: клиент отправляет отрицание отдельной операцией «не в диапазоне» с границами от начала дня до начала следующих суток, а сервер отрицает переданный полуинтервал целиком. Из выборки уходят все записи этого дня, а не только полночь; записи без значения остаются. В интерфейсе условие при этом остаётся «Не равно» с `00:00`, а не превращается в отдельный пункт списка операций. «Не равно» с набранным вручную временем (например, `12:30`) сравнивается точно и исключает только эту минуту.

Если у колонки Лукап или Мультилукап выбран тип фильтра «Выбор значений» (для обоих типов он же и по умолчанию, второй возможный — «Текстовый»), отбор идёт по самим выбранным задачам, а не по их отображаемому тексту. Клиент присылает фильтр с дискриминатором `filterType` = `taskIdSet`; в DTO `TaskIdSetFilter` помимо общих полей `ColumnFilter` есть собственные поля: `taskIds` — список числовых идентификаторов задач, `blank` — признак отбора задач без значения ДП (описан ниже) и `labels` — необязательный список подписей выбранных задач, параллельный `taskIds`. В построении отбора `labels` не участвует: сервер хранит подписи вместе с фильтром в составе представления и возвращает их при чтении без изменений, чтобы чип над таблицей показывал названия, а не номера. Запрос без `labels` разбирается как раньше — поле десериализуется в `null`. Сервер строит из него один узел фильтра: операция `=`, в `ParamType` — обозначение типа колонки, которое понимает хранимая функция: `LookupValue` для Лукапа и `MultiSlctSubcatTasks` для Мультилукапа (в кэше ДП тип Лукапа называется `LookUpField`; `ColumnFilterService.GetFilter` переименовывает его в `LookupValue` до разбора фильтра, поэтому в разборе `taskIdSet` встречается уже переименованное значение), в `DecimalValue` — набор идентификаторов через запятую с префиксом `any:` — у Лукапа и Мультилукапа одинаково: подходит задача, у которой стоит хотя бы одно из отмеченных значений (объединение по «или»), поэтому несколько отмеченных значений расширяют выборку, а не сужают её. Текстовое значение (`RawValue`) не заполняется. Хранимая функция сопоставляет набор с `SelectedTaskID` в таблице выбранных задач `ExtParamValueSelectedTasks`.

**Перевод сохранённых настроек.** Настройки таблиц категории — и административные наборы колонок, и пользовательские виды — при обновлении на 2.268 один раз переведены с типа фильтра «Текстовый» на «Выбор значений» у колонок Лукап и Мультилукап. Замена выполнена в самих сохранённых настройках, подмены при чтении нет, и колонки остальных типов не затронуты. Перевод одноразовый: если после обновления вернуть колонке тип «Текстовый» и сохранить вид, этот выбор сохраняется.

**Откуда берётся список значений.** Значения для выбора отдаёт отдельная точка `POST api-core/datasource/{type}/{entityId}/lookup-values`: список строится из фактических значений колонки у задач, которые пользователь видит в этом гриде, с тем же отбором и тем же пользователем, что и страница грида. Поэтому в списке нет значений из недоступных задач, и наоборот — права на категорию-источник для показа списка не требуются. Запрос принимает имя колонки, строку поиска и постраничные `skip` и `take` (по умолчанию 50); ответ содержит страницу значений и признак `hasMore`, общее число не считается. Колонка дополнительного параметра с ограничением доступа даёт пустой список, как и при группировке. Операции по такой колонке проверяются на сервере, поэтому запрет не обходится прямым запросом мимо интерфейса. Колонка не того типа, параметр вне категории или источник типа «Таблица» — ошибка `400`, отсутствие прав на грид — `403`.

**Когда уходит запрос и где работает этот источник.** Список запрашивается при первом раскрытии попапа фильтра, а не при открытии представления: у сохранённого вида с уже применённым отбором «Выбор значений» запрос уходит только когда пользователь откроет фильтр. Источник подключён у грида категории и грида раздела; остальные хосты грида берут список прежним способом — задачами категории-источника под правами пользователя на неё, поэтому там пустой список у пользователя без прав на категорию-источник остаётся штатным поведением.

Перед построением набор нормализуется: повторы схлопываются, нулевые и отрицательные идентификаторы отбрасываются. Остальные случаи дают ошибку запроса (HTTP 400), а не молчаливый показ неотфильтрованного грида:

- `taskIds` не передан и признак `blank` не выставлен — ошибка «Фильтр taskIdSet требует список TaskIds». В живом запросе данных такой фильтр не отбрасывается при разборе и доходит до этой проверки; при чтении сохранённого представления он отбрасывается санитайзером, чтобы вид открывался;
- тип ДП колонки не Лукап и не Мультилукап — ошибка «Фильтр taskIdSet не поддерживается для типа ДП …»; тип проверяется раньше содержимого набора;
- после нормализации не осталось ни одного положительного идентификатора, а исходный набор был непустой — ошибка «Фильтр taskIdSet требует хотя бы один положительный TaskID». Из интерфейса такой набор не приходит: состояние «ничего не отмечено» уходит пустым списком без мусорных значений;
- идентификаторов больше предела — тело ответа содержит код `taskIdSetLimitExceeded` и предельное число в `maxAllowed`; набор не усекается. Предел задаёт настройка `TaskIdSetFilterMaxItems`, при отсутствии или неположительном значении он равен 1000.
- значение поля `taskIds`, `labels`, `excludeTaskIds` или `excludeLabels` прислано в форме, которую контракт не разбирает, — ошибка запроса `400` с именем колонки и поля. Разбор мягкий, как у самого контракта: строки-числа в списке идентификаторов и числа в списке подписей принимаются. Проверка идёт до обращения к данным; в чтение сохранённого представления она не попадает — негодный фильтр там отбрасывает санитайзер.

**Признак «без значения ДП».** Поле `blank` в `TaskIdSetFilter` включает отбор задач, у которых у ДП нет значения. Признак работает только для колонок Лукап и Мультилукап: тип ДП проверяется раньше содержимого запроса, поэтому для остальных типов запрос отклоняется той же ошибкой про неподдерживаемый тип. JSON без поля `blank` разбирается в `false`, так что для клиентов, которые признак не присылают, поведение прежнее.

| `blank` | `taskIds` | Что строит сервер |
|---|---|---|
| не передан или `false` | непустой | один узел `=` по набору идентификаторов, как описано выше |
| не передан или `false` | не передан | ошибка «Фильтр taskIdSet требует список TaskIds» |
| не передан или `false` | передан пустой | заведомо ложное условие `1 = 0` — ноль строк: состояние «ничего не отмечено» |
| `true` | не передан или пустой | один узел `is null` по тому же `ParamType` — задачи без значения ДП |
| `true` | непустой | два узла под «или»: `is null` и обычный отбор по набору, с префиксом `any:` — у Лукапа и Мультилукапа одинаково |
| `true` | непустой, но после нормализации пустой | ошибка «Фильтр taskIdSet требует хотя бы один положительный TaskID» |

Семантика объединения внутри набора (`any:`) та же, что и без признака. Признак `blank` отправляет и интерфейс: в фильтре «Выбор значений» колонок «Ссылка» и «Множественная ссылка» он отвечает за пункт «(Пусто)» — отмеченный пункт уходит как `blank: true`, снятый в режиме «всё, кроме снятых» — как `blank: false`. Отбор задач без значения ДП пользователь закрывает и другим способом: тип фильтра «Текстовый», оператор «Нет значения».

**Режим «всё, кроме снятых».** Фильтр «Выбор значений» у колонки Лукап работает в двух режимах: «Все» — отмечено всё, кроме снятых, и «Только отмеченные» — прежнее поведение. Режим задаёт параметр фильтра колонки, который ставит только маппер колонок Лукапа; у Мультилукапа и в гриде подписей параметра нет, и режим там прежний. В режиме «Все» клиент присылает в том же `TaskIdSetFilter` вместо `taskIds` два поля: `excludeTaskIds` — числовые идентификаторы снятых задач (сам факт наличия поля означает режим исключений) и `excludeLabels` — параллельный список подписей снятых задач, он участвует только в хранении и показе чипа. `taskIds` и `excludeTaskIds` в одном фильтре не допускаются.

| `excludeTaskIds` | `blank` | Что строит сервер |
|---|---|---|
| непустой | не передан или `false` | `is not null` под «и» с исключением набора: строка проходит, если у ДП есть значение и ни одно её значение не входит в набор |
| непустой | `true` | то же исключение под «или» с `is null`: строки без значения ДП остаются в выборке |
| пустой | `false` | один узел `is not null` — только строки со значением ДП |
| пустой | `true` | ошибка «Фильтр taskIdSet с пустым excludeTaskIds и blank=true не ограничивает выборку»; интерфейс такой фильтр не отправляет |

Режим исключений работает только для колонок Лукап: у Мультилукапа и в гриде подписей его нет — параметр, переводящий фильтр в «всё, кроме снятых», ставит только маппер колонок Лукапа. Оба списка сразу и набор из одних неположительных идентификаторов — ошибка запроса `400`, как и прежде. Предел набора не изменился: настройка `TaskIdSetFilterMaxItems`, при отсутствии или неположительном значении — 1000; превышение даёт `taskIdSetLimitExceeded` с предельным числом в `maxAllowed`.

**Чего режим не требует.** Условие собирается штатными узлами разбора — `<>` с набором `any:` (как «Исключая значение» у текстового фильтра Лукапа) и `is null` / `is not null`, — поэтому под режим «всё, кроме снятых» новых объектов в базе и миграции не требуется.

**Сохранённый вид с негодным отбором.** При чтении настроек санитайзер отбрасывает ровно те `taskIdSet`-фильтры, на которых живой отбор ответил бы ошибкой, — иначе вид открывался бы, а первый же запрос данных по нему падал. Отбрасываются: `taskIds` не передан и `blank` не выставлен; после нормализации не осталось ни одного положительного идентификатора, а исходный набор был непустой — в обоих режимах; оба списка сразу; `taskIdSet` на колонке, для которой контракт не заведён (для списка отмеченных — не Лукап и не Мультилукап, для режима исключений — не Лукап); пустой `excludeTaskIds` с `blank: true`; идентификаторов больше предела. Отборы, законные на живом запросе, остаются в виде как есть, в том числе `taskIds: []` без `blank` («ничего не отмечено», ноль строк) и пустой `excludeTaskIds` с `blank: false` (только строки со значением ДП).

**«Выбрать все».** Без поиска пункт переключает два состояния: «всё отмечено» и «ничего не отмечено» (ноль строк). При заданном поиске — у Лукапа и Мультилукапа — меняются только найденные значения, причём со всех страниц: клиент догружает их страницами по 500 (верхний предел точки `lookup-values`), обычная прокрутка списка при этом остаётся по 50. Если найденных больше 1000, отметки не меняются, а в фильтре выводится сообщение о необходимости уточнить поиск. Состояние значений вне поиска сохраняется; в режиме «Только отмеченные» новые отметки объединяются с прежним выбором.

**Пункты контекстного меню на таких колонках.** «Фильтр по значению» и «Исключая значение» на колонке Лукап или Мультилукап с фильтром «Выбор значений» шлют не `taskIdSet`, а текстовую модель: `filterType` — `text`, операция — `equals` или `notEqual`, а идентификаторы значения ячейки лежат в `customInfo` (`{ taskId, option }`). Разбирает её `MultiSelectFilter` (`core/Valhalla.Services/AgGridServices/Filters/MultiSelectFilter.cs`): список из `option` подменяет собой текст фильтра и уходит в условие по идентификаторам задач (`DecimalValue`), поэтому сравнение идёт по связанным задачам, а не по отображаемому тексту. Префикс в `option` задаёт режим отбора (`any:` при его отсутствии, `all:`, `exact:` — см. выше); Мультилукап в обоих режимах отправляет `exact:<ID>,<ID>` — набор значений ячейки сравнивается целиком, Лукап при исключении отправляет голый список идентификаторов. Исключение строится как «значение пустое ИЛИ условие не выполнено», поэтому строки с незаполненной ячейкой остаются в выборке. Несовместимое значение `option` (пустой список после префикса, нечисловой остаток) фильтром не считается: условие не строится, в журнал уходит предупреждение.

**Поле `taskId` в текстовой модели фильтра необязательно.** В модели, которая приходит из пункта меню ячейки, идентификатор задачи лежит в `customInfo.taskId`, но приходит он не всегда: значение может быть пустым, например у значения фильтра без задачи. Разбор ведёт отдельная модель, и её поле объявлено обнуляемым. Раньше поле было обычным целым, и такая модель роняла разбор ошибкой приведения `null` к числу — запрос данных грида отвечал `500`. Теперь такой вход разбирается штатно: отбор строится по самому значению фильтра так же, как при заполненной задаче, — на выборку поле не влияет.

## Дерево FilterNode

Фильтры строят дерево `FilterNode`:

```
GeneralFilter (root, AND)
├── FilterNode (column=State, operand=IN, values=[1,3])
├── FilterNode (column=ExtParam42, operand=CONTAINS, value="test")
└── GeneralFilter (OR)
    ├── FilterNode (column=Owner, operand=EQUALS, value=123)
    └── FilterNode (column=Performer, operand=EQUALS, value=456)
```

Дерево передаётся в SP `ShowTasksFeed` / `ShowExtParamTableData` и транслируется в WHERE-clause.
