Наложение ограничений на лукапы - важная задача. Она появляется тогда, когда нужно отфильтровать список записей, из которого выбирается значение лукапа. Так, например, положим, что нельзя совмещать должности менеджера и кладовщика. Иными словами, тот, кто принят на должность кладовщиком, не могут быть выбраны как менеджер заказа, и наоборот.
Наложение ограничений на простой лукап
Реализуем первое ограничение: исключим тех, кто работает кладовщиком, из списка работников, которых можно выбрать в качестве менеджера заказа. Для этого откроем контроллер формы редактирования заказа:
app → controllers → i-i-s-shop-order-e.js
Для того, чтобы наложить ограничение на лукап, нужно создать соответствующий предикат. Необходимый нам предикат является простым (нужно проверить значение конкретного свойства - должности). Простые, ни от чего не зависящие предикаты можно создать в хуке init (вызывается автоматически при создании контроллера) через метод set:
где managerLimitPredicate - имя нового свойства контроллера,
position - проверяемое свойство модели,
eq - оператор равенства,
Manager - необходимое значение свойства модели.
Остается только передать созданное ограничение (фильтр) в виде предиката для лукапа. Для этого нужно добавить атрибут в соответствующий template:
app → templates → i-i-s-shop-order-e.js
Проверим, работает ли предикат:
Все верно, в списке оказались лишь те сотрудники, у которых указана должность “Менеджер”.
Самостоятельно создайте предикат для лукапа:
Склад
Кладовщик - только сотрудники с должностью “кладовщик”.
Наложение ограничений на лукапы в groupedit
Более сложной задачей является наложение предиката на определенные поля в компоненте для работы с детейлами (groupedit). Рассмотрим данную задачу на примере ограничения выбора товаров в заказе: если товар уже один раз выбран в рамках заказа, то больше его в списке для выбора товаров выводить не будем.
Для того, чтобы ограничить лукап в groupedit, необходимо указать соответствующий предикат в функции getCellComponent в контроллере:
app → controllers → i-i-s-shop-order-e.js
Для настройки компонентов, в частности лукапов, которые будут отображаться в ячейках groupedit, в контроллере соответствующей формы редактирования генерируется требуемый код в методе getCellComponent. В данном случае мы можем видеть установку перечня свойств, аналогичных свойствам обычного лукапа: сюда мы и добавим наш предикат.
Дополнительная сложность заключается в том, что предикат должен быть динамическим, т.е. меняться при определенных обстоятельствах. В нашем случае таким обстоятельством является взаимодействие с самим лукапом “Товар” в строке заказа: при выборе очередного товара, этот товар должен учитываться в предикате и больше не выводиться.
Рассмотрим события, при которых должен изменяться предикат:
- открытие формы (если есть уже в списке детейлов какие-то товары, то предикат должен сразу ограничить их список для новых строк);
- добавление новой строки;
- удаление строки;
- очистка лукапа.
Каждое из этих событий должно триггерить изменение предиката. Реализуем механизм для пересчета предиката и воспользуемся им последовательно в каждом из этих событий.
Механизм формирования предиката
Для того, чтобы применить актуальный предикат к лукапу Товара в определенный момент времени, необходимо выполнить три шага:
- получить список значений определенного свойства модели Товар, из перечня товаров, которые уже используются;
- вычислить предикат на основе полученного списка значений;
- передать предикат в лукап.
Второй шаг (вычисление предиката) является потенциально универсальным: он может применяться не только для текущего лукапа, но и для других, в том числе на других формах. Чтобы не копировать каждый раз один и тот же код, создадим утилиту, вычисляющую предикат по списку значений и имени свойства:
ember generate util generate-predicate-by-list
Утилиты используются в тех случаях, когда необходимо простые функции использовать во многих местах кода. Утилиты доступны в любом месте ember-приложения и подключаются с использованием ключевого слова import.
Заменим содержимое утилиты следующим кодом:
В рассмотренной функции, которую будем называть основной, формируется предикат по принципу “цепочки”, звенья которой добавляются последовательно, сцепляемые определенным логическим оператором (и / или):
На данный момент логика работы лукапов имеет данное ограничение в связи с особенностями использования протокола OData, и преодолеть его без изменения логики работы самого компонента не представляется возможным. В подобных случаях, которые на практике могут возникнуть не так часто, можно попробовать, как один из вариантов, наложить соответствующее ограничение на требуемый список записей на уровне сервера.
В связи с наличием данного ограничения условимся, что список товаров в нашем приложении не является очень большим.
На базе основной функции построим еще две последовательные: функцию ИЛИ (с конкретным логическим оператором, в отличие от базовой) и отрицающую логическую ИЛИ. Вторая функция нам нужна для того, чтобы воплотить конструкцию типа
“не( ЗНАЧЕНИЕ 1 ) и не( ЗНАЧЕНИЕ 2 ) и …. и не( ЗНАЧЕНИЕ N )”
Если её реализовывать “в лоб”, то получится довольно сложная конструкция. Из логики известно следующее утверждение:
не( А или Б ) = не( А ) и не( Б )
Им и воспользуемся при построении необходимого предиката:
Теперь вызовем функцию generateNotOrPredicateByList в контроллере:
Утилита, возвращающая предикат нужного нам вида, написана и доступна теперь из контроллера. Вынесем установку свойств лукапа в хук init в переменную productProperties и получим её значение в методе getCellComponent:
Теперь при изменении свойства контроллера productProperties эти изменения будут динамически применяться к лукапу Товара. По умолчанию значением свойства lookupLimitPredicate является undefined: такое значение этого свойства подразумевает отсутствие ограничений на выборку.
Добавим перед методом getCellComponent функцию setProductLookupPredicate() для переопределения предиката:
Функция setProductLookupPredicate() по сути представляет собой сеттер, который меняет значение конкретного атрибута свойства productProperties. Таким образом, при вызове данной функции предикат будет пересчитан на основании текущего списка выбранных товаров и применен к лукапу как динамическое свойство.
Открытие формы
Для того, чтобы процесс формирования предиката запускался вместе с загрузкой формы, нужно его вызвать в момент, когда модель уже загружена. Для этого перейдем в соответствующий роут формы редактирования Заказа и кастомизируем хук setupController():
app → routes → i-i-s-shop-order-e.js
Так как это стандартный хук Ember.js, то при кастомизации нужно не забыть вызвать выполнение базовой логики.
Проверим, отрабатывает ли предикат при загрузке страницы. Для этого перейдем в один из ранее созданных заказов (например, заказ 2):
Предикат отработал корректно: среди вариантов товара новой строки товар с кодом 2 (выбранный ранее) отсутствует.
Добавление новой строки
Наиболее целесообразно будет привязать данное событие к событию закрытия окна лукапа: это событие легко отслеживается из контроллера. Для того, чтобы обработать это событие, добавим следующий код:
Теперь в методе _setLookupPredicate необходимо выбрать лукап, на который мы будем обращать внимание при закрытии модального окна. Дело в том, что в данный момент эта функция будет вызываться при закрытии любого лукапа, а нас интересует вполне конкретный.
На вход данному методу (в параметр componentName) будет приходить имя лукапа: проверим это имя. Для этого выведем в консоль параметр componentName:
Далее инициируем событие закрытия лукапа для выбора товара, расположенного в ячейке groupedit, закрыв соответствующее модальное окно:
Копируем имя компонента целиком и вставим его в условие для проверки имени компонента в обработчике закрытия модального окна лукапа:
Проверим, работает ли изменение предиката на добавление строки groupedit (на примере нового заказа):
Все работает корректно.
Удаление строки заказа
При удалении строки в groupedit так же, как и при закрытии окна лукапа, возникает особое событие в соответствующем сервисе - olvRowDeleted. Его также можно подключить при помощи метода inject и использовать для привязки кастомной логики:
Уточним через консоль имя componentName, которое приходит в функцию _setLookupPredicate() и добавим его в конструкцию switch с тем же телом, что и в предыдущем случае:
На первый взгляд, этого должно быть достаточно. Однако, если мы проверим сейчас работоспособность кода, то лукап не будет учитывать удаленную запись. Почему так получается? Дело в том, что указанное нами событие и событие удаления записи происходят синхронно, а значит, на момент отработки нашей кастомной логики запись еще существует.
Для того, чтобы исправить этот момент, добавим в логику следующий код:
Проверим работу кода на примере заказа 1:
Все работает корректно.
Очистка лукапа
Для отслеживания очистки лукапа нет особого события среди событий в сервисе лукапа, чтобы можно было применить решение аналогичное предыдущим случаям. Однако, среди свойств лукапа, установку которых мы вынесли в хук init(), можно найти имя соответствующего экшна:
Дополним его кастомной логикой:
Проверим работу кода на примере заказа 1:
Все работает корректно.
Самостоятельно настройте лукап для поля “Заказ” Накладной, чтобы нельзя было создать несколько накладных на один заказ.
Подсказка: воспользуйтесь вычиткой данных через builder, чтобы получить сведения о всех существующих накладных и их заказах.
Итог
Установление предикатов на лукапы - часто совершаемая операция. Она необходима для более точной передачи логики выбора одного из нескольких значений, а также для исключения человеческих ошибок при работе с приложением.
При создании предикатов для лукапа всегда продумывайте варианты событий, при которых данный предикат должен пересчитываться, а также то, статическим или динамическим должен быть предикат.