- Главная [object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
- Начало работы
- Работа с типами
- Глобальный контекст и изоляция
- Работа с приложениями
- Массовые действия с элементами приложения
- Работа с внешними сервисами
- Скрипты в виджетах
- Веб компоненты
- Права доступа
- Начало работы с процессами
- Начало работы с подписями
- Начало работы с предпросмотром файлов
- Начало работы с организационной структурой
- Начало работы с пользователями и группами
- Начало работы с типом данных Таблица
- Как решить вашу задачу
- Как сделать пользовательское отображение элементов приложений
- Как сделать динамическое отображение полей/виджетов со сложным условием
- Как регистрировать документ
- Как рассчитывать интервал между датами
- Как создать замещение пользователя
- Как использовать пагинацию и сортировку при поиске элементов приложения
- API
- Типы объектов
- Типы данных
- Глобальные константы
- Работа с приложениями
- Веб-запросы
- Права доступа
- Документооборот
- Линии
- Виджет «Код»
- Подписи
- Рабочие календари
- Интеграция с IP-телефонией
- Интеграция с сервисами рассылок
В этой статье
Как сделать пользовательское отображение элементов приложений
Иногда бывает необходимо представить список элементов приложения в более удобном виде, чем он доступен по умолчанию. Более того, необходимо добавить данные из других приложений, связанные с этими элементами. Таким образом можно настроить динамические отчеты, дашборды и т. п.
Допустим, в компании есть приложение Материалы, в котором хранится список материалов для строительно-монтажных работ.
Также в компании есть приложение Заказы, в котором элементами являются заказы от клиентов на поставку материалов. На форме приложения есть свойство типа Таблица, которое заполняется заказанными товарами.
Необходимо создать отдельную сводную таблицу, в которой указано, какие материалы требуются по заказам. В этой таблице должно быть отображено, сколько каких материалов сейчас требуется по всем заказам в общем и по каждому в отдельности.
Для этого создадим новую страницу и назовем ее Требуемые материалы. Изначально на страницу вынесен только виджет Код, с помощью которого можно задать нужное отображение элементов.
Скрипт
Определение структуры данных
Для удобной верстки страницы сначала следует определить, в каком виде мы хотим получить результат. Исходя из необходимой структуры данных, создадим интерфейсы, которые можно будет дорабатывать в процессе. Например, мы знаем, что в первую очередь потребуется знать название каждого материала (
materialName
) и требуемое количество материала по заказам (quantity
), а также видеть список заказов, содержащих этот материал (deals
):interface NeedForMaterials { materialName: string; quantity: number; deals: any[]; }
Детальной информации по заказам на этой странице отображать не требуется — необходимо знать только название заказа (
dealName
) и количество материала (quantity
). Добавим также ID, чтобы в случае необходимости достоверно идентифицировать отдельный заказ:interface NeedByDeal { dealId: string; dealName: string quantity: number; }
Преобразуем основной интерфейс, указав, какого типа будет массив информации по заказам:
interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; }
В дизайнере интерфейсов перейдем на вкладку Скрипты. В данном случае скрипт должен выполняться только на клиенте. Добавим созданные интерфейсы в код скрипта.
Для того чтобы хранить и обрабатывать список требуемых материалов в удобном виде, создадим свойство
materialNeeds
. Оно будет представлять собой массив требуемых материалов, благодаря чему для обработки информации станут доступны методы массивов. Их можно использовать, например, для фильтрации:let materialNeeds: NeedForMaterials[];
Получение и обработка данных
Напишем функцию, которая будет заполнять список требуемых материалов данными:
Используя глобальную константу Namespace, вызовем у нужных приложений метод поиска элементов
search()
, где зададим ограничение для количества элементов в 1000 (максимально возможное ограничение равно 10000):const materials = await Namespace.app.materials.search().size(1000).all(); const deals = await Namespace.app.deals.search().size(1000).all();
Получилось два асинхронных запроса, которые независимы друг от друга, и их необходимо выполнить одновременно. В связи с этим оптимальным будет запускать их параллельно. Используем метод
Promise.all()
, который объединяет массив нескольких обещаний в одно обещание. Дождемся результата и декомпозируем его.const [materials, deals] = await Promise.all([ Namespace.app.materials.search().size(1000).all(), Namespace.app.deals.search().size(1000).all(), ]);
materialy
), является пустым, то пропускаем этот шаг для элемента и переходим к следующему. Если же значение свойства определено, необходимо перебрать строки таблицы. Они содержат неполную информацию, но из них можно извлечь идентификатор материала. По нему найдем нужный материал в списке.for (const deal of deals) { if (!deal.data.materialy) { continue; } for (const tableItem of deal.data.materialy) { const material = materials.find(material => material.id === tableItem.materialy.id); } }
NeedByDeal
:const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.kolichestvo, };
materialNeeds
данные о том, сколько данного материала требуется по конкретному заказу. В строке таблицыtableItem
есть столбец типа Приложение, ссылающийся на Материалы (materialy
). Из него мы можем получить нужный идентификатор. Используя его, проверим, есть ли этот материал в списке требуемых. Если нет, то добавляем его, указывая общую информацию.Далее формируем информацию о требуемых материалах по конкретной сделке
dealNeeds
. Добавляем эту информацию в список заказов по данному материалу. Увеличиваем общее количество требуемого материала на количество по данному заказу.const material = materials.find(material => material.id === tableItem.materialy.id); let need: NeedForMaterials | undefined = materialNeeds.find(need => need.materialId === tableItem.materialy.id); if (!need) { need = { materialId: tableItem.materialy.id, materialName: material!.data.__name, quantity: 0, deals: [], showDeals: true, }; materialNeeds.push(need); } const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.kolichestvo, clientId: deal.data.client?.id, }; need.quantity += tableItem.kolichestvo; need.deals.push(dealNeeds);
Формирование результата
Так как в функции заполнения данных есть асинхронное получение элементов приложений, то и сама функция будет асинхронной. Однако нам необходимо при загрузке страницы дождаться завершения этой асинхронной функции, иначе вначале на странице не будет ничего отображаться, а затем потребуется обновление. В данном случае это не нужно, поэтому дождемся результата еще на этапе инициализации (загрузки) страницы.
Для этого в скриптах создадим функцию, которая будет автоматически выполняться при инициализации. В ней необходимо выполнить асинхронную функцию заполнения данными с ожиданием результата. Тогда к моменту непосредственного рендера страницы все данные уже будут подготовлены.
Для удобства получения данных из скриптов добавим функцию
getNeeds()
, которая возвращает текущее значениеmaterialNeeds
.Итоговый результат клиентского скрипта:
let materialNeeds: NeedForMaterials[]; interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; } interface NeedByDeal { dealId: string; dealName: string quantity: number; } async function onInit(): Promise<void> { await fillNeeds(); } function getNeeds() { return materialNeeds; } async function fillNeeds() { materialNeeds = []; const [materials, deals] = await Promise.all([ Namespace.app.materials.search().size(1000).all(), Namespace.app.deals.search().size(1000).all(), ]); for (const deal of deals) { if (!deal.data.materialy) { continue; } for (const tableItem of deal.data.materialy) { const material = materials.find(material => material.id === tableItem.materialy.id); const dealNeeds = <NeedByDeal> { dealId: deal.id, dealName: deal.data.__name, quantity: tableItem.kolichestvo, }; if (!materialNeeds[tableItem.materialy.id]) { materialNeeds[tableItem.materialy.id] = <NeedForMaterials> { materialName: material!.data.__name, quantity: 0, deals: [], } } materialNeeds[tableItem.materialy.id].quantity += tableItem.kolichestvo; materialNeeds[tableItem.materialy.id].deals.push(dealNeeds); } } }
Шаблон виджета
Добавляем на страницу виджет Код. С его помощью можно воплотить любые идеи по внешнему виду страницы, в том числе динамическое отображение данных, с помощью HTML, CSS и вставки кода. Для примера попробуем отобразить требуемые материалы в виде таблицы. Добавим основу таблицы с двумя колонками — Материал и Количество. Используем контейнер
<tbody></tbody>
для тела таблицы.Внутри
tbody
используем цикл для обработки требуемых материалов, полученных с помощью функцииgetNeeds()
. По каждому элементу рендерим шаблонrenderNeed
. В качестве аргумента передаем сам элемент списка требуемых материалов, который имеет структуру, соответствующуюNeedForMaterials
:<table> <thead> <tr> <th>Материал</th> <th>Количество</th> </tr> </thead> <tbody> <% for (const need of getNeeds()) { %> <%= renderNeed(need) %> <% } %> </tbody> </table>
В шаблоне
renderNeed
по каждому материалу добавим строку таблицы<tr class="material-need-deals">
, а в ячейкахtd
добавим название материалаneed.materialName
и общее количество по заказамneed.quantity
.Затем, если по данному материалу есть заказы
need.deals
, то по каждому из них циклом добавляем строку<tr class="material-need-deals">
. В её ячейках указываем название заказаdeal.dealName
и требуемое количество материала по немуdeal.quantity
.<% $template renderNeed(need) %> <tr class="material-need-title"> <td><%- need.materialName %></td> <td><%- need.quantity %></td> </tr> <% if (need.deals.length) { %> <% for (const need of getNeeds()) { %> <tr class="material-need-deals"> <td class="material-need-deals__name"><%- deal.dealName %></td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
Страница практически готова, но нужно доработать её внешний вид, чтобы облегчить восприятие информации. Для этого в начало содержимого виджета Код можно добавить CSS-стили внутри контейнера
<style> ... </style>
, который будет применяться к таблице по тэгам и классам элементов. Чтобы было удобнее, можно назначить отдельный класс для таблицы или же обернуть её в контейнер с нужным классом.Рассмотрим полное содержимое виджета Код после применения стилей и улучшения внешнего вида:
<style> .needs-container table { width: 100%; } .needs-container thead { border-top: 1px solid #d9d9d9; border-bottom: 1px solid #d9d9d9; } .needs-container th, .needs-container td { padding: 18px 6px; } .needs-container th { color: #8c8c8c; } tr.material-need-title { font-weight: bold; background-color: #e3eef7; } tr.material-need-deals-title-row td { padding-top: 0px; padding-bottom: 0px; } td.material-need-deals__name { padding-left: 36px; } </style> <div class="needs-container"> <table> <thead> <tr> <th>Материал</th> <th>Количество</th> </tr> </thead> <tbody> <% for (const need of getNeeds()) { %> <%= renderNeed(need) %> <% } %> </tbody> </table> </div> <% $template renderNeed(need) %> <tr class="material-need-title"> <td><%- need.materialName %></td> <td><%- need.quantity %></td> </tr> <% if (need.deals.length) { %> <% for (const deal of need.deals) { %> <tr class="material-need-deals"> <td class="material-need-deals__name"><%- deal.dealName %></td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
Результат рендера на странице:
Динамика
Попробуем доработать созданный отчет и, чтобы было удобнее с ним работать, добавим немного динамики. Для начала дадим возможность перейти к просмотру нужного материала или заказа при нажатии на его название в таблице. Для этого мы обернем вставку названия материала и заказа в ссылку, чтобы получилась строка типа: «(p:item/код_раздела/код_приложения/идентификатор_элемента)»:
<a href="(p:item/installation_orders/materials/<%- need.materialId %>)"> <%- need.materialName %> </a>
<a href="(p:item/installation_orders/deals/<%- deal.dealId %>)"> <%- deal.dealName %> </a>
Теперь добавим возможность сворачивать и разворачивать список заказов по материалу:
showDeals
типа boolean, которое будет отвечать за видимость заказов по данному материалу:interface NeedForMaterials { materialName: string; quantity: number; deals: NeedByDeal[]; showDeals: boolean; }
В шаблоне рендера пункта списка требуемых материалов перед ссылкой на название материала добавим символы плюс и минус, один из которых отображается в зависимости от значения свойства
showDeals
данного пункта списка. По клику на элемент<span>
, который будет кнопкой для разворачивания и сворачивания списка заказов, добавим вызов функцииtoggleShowDeals
и передадим в нее идентификатор материала:onclick="<%= Scripts %>.toggleShowDeals('<%= need.materialId %>')"
. Стоит обратить внимание, что при вызове функции из скриптов в атрибутеonclick
можно передать только отдельное строковое свойство, а не весь объект целиком. Иначе будет некорректно воспринято написание символов < и > в аргументах функции.В условие видимости блока с выводом заказов по материалу добавим проверку свойства
need.showDeals
:<% if (need.showDeals && need.deals.length) { %>
.В итоге шаблон рендера пункта списка требуемых материалов будет выглядеть следующим образом:
<% $template renderNeed(need) %> <tr class="material-need-title"> <td> <span class="show-deals-toggle" onclick="<%= Scripts %>.toggleShowDeals('<%= need.materialId %>')" > <% if (need.showDeals) { %> - <% } else { %> + <% } %> </span> <a href="(p:item/installation_orders/materials/<%- need.materialId %>)"> <%- need.materialName %> </a> </td> <td><%- need.quantity %></td> </tr> <% if (need.showDeals && need.deals.length) { %> <% for (const deal of need.deals) { %> <tr class="material-need-deals"> <td class="material-need-deals__name"> <a href="(p:item/installation_orders/deals/<%- deal.dealId %>)"> <%- deal.dealName %> </a> </td> <td><%- deal.quantity %></td> </tr> <% } %> <% } %> <% $endtemplate %>
toggleShowDeals
, которая принимает идентификатор материала из виджета Код, по нему ищет в массивеmaterialNeeds
пункт списка требуемых материалов и меняет значение его свойстваshowDeals
на обратное:function toggleShowDeals (materialId: string) { const need = materialNeeds.find(need => need.materialId === materialId); if (!need) { return; } need.showDeals = !need.showDeals; }
При этом у нас должен обновиться внешний вид виджета. Для этого используем то, что при изменении значений свойств контекста, с которыми связан виджет, проверяется необходимость его полного пересчета и перерисовки. Добавим на вкладке Контекст дизайнера интерфейсов свойство, значение которого будем изменять — например, свойство
timestamp
типа Дата/время.Применим это свойство в виджете Код — неважно, как именно оно будет использовано, например, можно добавить пустое условие:
<% if (Context.data.timestamp) {} %>
В скриптах будем присваивать этому свойству новое значение в тот момент, когда необходима перерисовка виджета:
function toggleShowDeals (materialId: string) { const need = materialNeeds.find(need => need.materialId === materialId); if (!need) { return; } need.showDeals = !need.showDeals; Context.data.timestamp = new Datetime(); }
В результате в нашей таблице появляется возможность сворачивать и разворачивать список заказов по отдельному материалу:
Добавление виджетов в виджете «Код»
Добавим возможность фильтровать данные в таблице. Для этого перейдем во вкладку Контекст дизайнера и добавим свойства, которые будут использоваться для ввода значений фильтров. Для правильной работы их типы должны соответствовать типам полей, по которым необходимо фильтровать данные. Создадим свойства
zakaz
иzakazchik
:Для управления фильтрами добавим виджет Выпадающее окно (Widget.popover), которое будет открываться при нажатии на кнопку. Как добавить этот виджет или некоторые другие, описано в статье о виджетах. Для этого изменим заголовок таблицы и добавим виджет Выпадающее окно, который состоит из элемента открывания окна
filtersOpener
, содержимого окнаfiltersContent
и добавления самого виджета черезUI.widget.popover( ... )
.В содержимое окна добавим заголовок и вставим добавленные свойства View-контекста с помощью
UI.widget.viewContextRow( ... )
. Также добавим кнопку, нажатие на которую будет выполнять функцию, обрабатывающую значения полей для фильтрации<%= Scripts %>.applyFilters()
:<thead> <tr> <th> Материал <% $template filtersContent %> // Содержимое выпадающего окна <h4> Настройка фильтрации: </h4> <%= UI.widget.viewContextRow('zakaz', { name: 'Заказ' }) %> <%= UI.widget.viewContextRow('zakazchik', { name: 'Заказчик' }) %> <button type="button" class="btn btn-default" onclick="<%= Scripts %>.applyFilters()" > Применить </button> <% $endtemplate %> <% $template filtersOpener %> // Контейнер, клик по которому откроет окно <button type="button" class="btn btn-default btn-link">Настроить</button> <% $endtemplate %> // Добавление виджета <%= UI.widget.popover( filtersOpener, { type: 'widgets', size: 'lg', position: 'right', }, filtersContent ) %> </th> <th>Количество</th> </tr> </thead>
Теперь добавим функцию в клиентский скрипт:
async function applyFilters () { // Получаем текущие значения полей для фильтрации const zakaz = Context.data.zakaz; const zakazchik = Context.data.zakazchik; // Обновляем данные без фильтрации await fillNeeds(); if (!zakaz && !zakazchik) { // Если не выбран ни один из фильтров, то прекращаем выполнение — будут отображены все данные // Обновляем отображение, присвоив новое значение свойству Context.data.timestamp = new Datetime(); return; } // Создаем пустой массив для заполнения const filteredNeeds: NeedForMaterials[] = []; for (const need of materialNeeds) { // Для каждого материала обнуляем количество и создаем массив для отфильтрованных заказов need.quantity = 0; const filteredDeals: NeedByDeal[] = []; for (const deal of need.deals) { if ( (!zakaz || deal.dealId === zakaz.id) && (!zakazchik || zakazchik.id === deal.clientId)) { // Если заказ соответствует условиям фильтрации, добавляем в список // и вставляем количество требуемого материала по сделке. filteredDeals.push(deal); need.quantity += deal.quantity; } } // Обновляем заказы отфильтрованным списком need.deals = filteredDeals; if (need.quantity) { // Если требуемое количество данного материала не равно нулю, добавляем в список filteredNeeds.push(need); } } // Обновляем основной список требуемых материалов, используя отфильтрованные значения // и обновляем отображение, присвоив новое значение свойству `timestamp` materialNeeds = filteredNeeds; Context.data.timestamp = new Datetime(); }
Попробуем использовать созданные фильтры. При нажатии на ссылку Настроить в заголовке таблицы открывается окно с полями для фильтрации. Для начала установим фильтрацию по полю Заказчик, равное Заказчик 2:
После нажатия на кнопку Применить окно с фильтрами закрывается, а обновленный список содержит требуемые материалы только по заказам Заказчика 2:
Снова откроем окно с фильтрами и включим фильтрацию по полю Заказ, введя Заказ №2 с Заказчик 2:
Обновленная таблица содержит требуемые материалы только по Заказу №2 и Заказчику 2: