Перейти к содержанию

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

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

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

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

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

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

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

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

Фильтр колонки в ДП Таблица: операторы «Содержит», «Начинается с», «Пусто» и другие

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

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

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

Проверка входа запроса. Имена колонок сортировки и ключи фильтров, приходящие в запросе, проверяются до проверки на признаки 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
Through Особая обработка N/A Сквозной ДП: действующий тип сервер берёт у параметра-источника в конце цепочки и по нему готовит значение фильтра, но в хранимую процедуру передаёт тип Through — процедура сама определяет целевой тип и применяет правильную логику. Замена на SelectUsersOrgUnits/SelectUsersGroups происходит только при группировке, не при фильтрации. Колонка, которая ссылается на удалённый или недоступный параметр, фильтр не ломает: условие по ней не применяется, остальные условия работают как обычно. Если у сквозного параметра источник не определяется и колонка доступна, фильтр прерывается сообщением о неверной настройке сквозного параметра — цепочку правит администратор. Типичные применения: Through → ДП «Выбор пользователей» (Менеджер клиента, РП, BDM и др.)
File ИСКЛЮЧЁН N/A Файловый ДП
Numerator стандартный стандартный Нумератор
Virtual (колонка таблицы) N/A VirtualColumnFilter Только внутри ДП Таблица

Тип фильтра как настройка колонки. В настройках табличного вида категории тип фильтра колонки — самостоятельный параметр: в сохранённых настройках колонки может лежать явно выбранный тип, а не только выведенный из типа ДП. Допустимый набор значений зависит от типа ДП колонки (см. таблицу в 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.