#1898: Собственные виджеты в Swarmica (веб-плагины)
Отредактирована: сегодняОписание
Swarmica позволяет встраивать собственные приложения в интерфейс системы на экранах работы с заявкой, статьей, учетной записью пользователя, компании, лицензии и актива в виде веб-плагинов.
Виджет, веб-плагин – это код HTML, который может использовать данные из контекста экрана (например, номер заявки, значение кастомного поля учетной записи пользователя и тд) в своей внутренней логике.
Примеры задач, которые решаются с помощью веб-плагинов:
- Отобразить ссылку на учетную запись пользователя в CRM, используя значение поля user.ext_id
- Отобразить в iframe раздел Личного Кабинета пользователя в приложении, динамически подставив в адрес iframe ID аккаунта
- Отобразить мини-приложение для прочтения текста статьи вслух
- Отобразить виджет для работы с лицензионным ключом в биллинге, для получения информации о лицензии и быстрых действий по продлению этого ключа
Веб-плагин может содержать любой код HTML, в том числе и блоки с Javascript и CSS, что позволяет реализовать практически любую логику любого frontend-приложения.
Для того, чтобы подключить возможность управления веб-плагинами, необходимо обновить систему до версии 6.0.x или новее и добавить фича-флаг ui_widgets.
Настройка веб-плагина
Чтобы добавить плагин в систему, выполните следующие действия:
- Зайдите в Настройки > Интеграции > Кастомные виджеты
- Нажмите Создать и выберите нужный раздел, в котором предполагается отображать виджет
- Задайте настройки виджета:
- Название (будет отображаться в интерфейсе в качестве заголовка панели с виджетом)
- Доступен для (указывает роли пользователей, которые будут видеть данный виджет)
- Код (непосредственно код виджета. См. Использование динамических данных контекста про возможность динамической параметризации кода)
- Нажмите Сохранить и введите ID объекта в системе, чтобы посмотреть, как будет выглядеть виджет на странице этого объекта (например, номер заявки):

- Чтобы включить отображение виджета, включите соответствующую настройку и нажмите Сохранить. Чтобы отрегулировать положение виджета на экране, зайдите в раздел Настройки > Поля и формы > Настройки экранов и переместите виджет в удобное для пользователей место:

ВНИМАНИЕ! Код виджета передается в браузер всех пользователей системы, которым, согласно настройкам, показывается виджет. НЕ ИСПОЛЬЗУЙТЕ перманентные токены для доступов API в коде виджета или любые авторизационные данные, которые могут позволить получить доступ к интегрируемым системам с повышенными привилегиями
Использование динамических данных контекста
При рендере (отображении) плагина для конечного пользователя системы, код проходит обработку шаблонизатора Jinja, куда в контекст передается объект, для которого данный плагин отображается. Таким образом, вы можете использовать любые атрибуты объекта, доступные в этом контексте.
Например, для того, чтобы использовать значение кастомного поля в учетной записи пользователя, для того, чтобы сгенерировать адрес для iframe, можно использовать следующий код:
<iframe src="https://my_crm/accounts/{{ user.custom_fields.get_value('CF_UID') }}" ></iframe><p></p>
Более подробно про переменные контекста можно узнать в статье "Как использовать переменные контекста".
Примеры плагинов
Ниже примеры виджетов, которые демонстрируют возможности этого механизма
Для чего: дать возможность сотрудникам поддержки делать локальные (не сохраняющиеся на сервер) пометки при работе с заявкой. Например, при разговоре с клиентом по телефону.
Внешний вид:

Код:
<style>
#notes-widget {
background: #fff8a6;
border-radius: 10px;
padding: 12px;
box-shadow:
0 3px 10px rgba(0, 0, 0, .15);
}
#notes {
min-height: 220px;
max-height: 300px;
overflow-y: auto;
outline: none;
font-family: "Segoe Print", "Comic Sans MS", cursive;
font-size: 15px;
line-height: 30px;
padding: 8px 10px;
background-color: transparent;
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 29px,
rgba(0,0,0,.15) 30px
);
white-space: pre-wrap;
color: black;
}
#notes:empty::before {
content: "Напишите что-нибудь...";
color: rgba(0,0,0,.4);
}
#notes::-webkit-scrollbar {
display: none;
}
#status {
margin-top: 8px;
text-align: right;
font-size: 12px;
color: #666;
}
</style>
<div id="notes-widget">
<div id="notes" contenteditable="true"></div>
<div id="status"></div>
</div>
<script type="module">
const STORAGE_KEY = `quick-notes:{{ticket.id}}`
const notes = document.querySelector("#notes")
const status = document.querySelector("#status")
notes.innerHTML = localStorage.getItem(STORAGE_KEY) ?? ""
let saveTimer
let statusTimer
function showStatus(text) {
status.textContent = text
clearTimeout(statusTimer)
statusTimer = setTimeout(() => {
status.textContent = ""
}, 1500)
}
function save() {
localStorage.setItem(STORAGE_KEY, notes.innerHTML)
showStatus("Сохранено ✓")
}
notes.addEventListener("input", () => {
clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
save()
}, 500)
})
notes.addEventListener("keydown", event => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault()
save()
}
})
</script>
Для чего: отобразить адрес контрагента на карте
Внешний вид: 
Код:
<div id="map-widget">
<div class="d-flex align-items-stretch gap-2 mb-2">
<input
type="text"
class="form-control form-control-sm"
id="map-query"
placeholder="Например: Москва"
/>
<button
type="button"
class="btn btn-primary btn-sm"
id="show-map"
>
GO
</button>
</div>
<div
id="map-container"
class="border rounded overflow-hidden"
style="height:400px"
></div>
</div>
<script type="module">
const input = document.querySelector("#map-query")
const button = document.querySelector("#show-map")
const container = document.querySelector("#map-container")
async function renderMap(query) {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`
)
const [place] = await res.json()
if (!place) {
container.innerHTML = `
<div class="d-flex align-items-center justify-content-center h-100 text-danger">
Ничего не найдено
</div>
`
return
}
const lat = Number(place.lat)
const lon = Number(place.lon)
container.innerHTML = `
<iframe
width="100%"
height="100%"
frameborder="0"
loading="lazy"
src="https://www.openstreetmap.org/export/embed.html?bbox=${lon-0.01},${lat-0.01},${lon+0.01},${lat+0.01}&layer=mapnik&marker=${lat},${lon}">
</iframe>
`
}
button.addEventListener("click", () => {
const query = input.value.trim()
if (!query) {
container.innerHTML = `
<div class="d-flex align-items-center justify-content-center h-100 text-secondary">
Введите место
</div>
`
return
}
renderMap(query)
})
input.addEventListener("keydown", e => {
if (e.key === "Enter") {
button.click()
}
})
renderMap("Moscow")
</script>
Для чего: дать возможность редакторам проверить доступность статьи для людей с ограниченными возможностями
Внешний вид:

Код:
<div id="voice-reader-widget" style="min-height: 65px">
<div>Статья #{{article.id}}</div>
<br>
<div class="d-flex gap-2">
<button type="button" class="w-50 btn btn-primary btn-sm" id="read-article">
🔊 Прочитать
</button>
<button type="button" class="w-50 btn btn-outline-secondary btn-sm" id="stop-reading">
⏹ Стоп
</button>
</div>
</div>
<script type="module">
const container = document.querySelector("#voice-reader-widget")
document.querySelector("#read-article").addEventListener("click", async () => {
console.log("click")
const data = await fetch("/api/articles/{{article.id}}/", {
headers: {
Authorization: `Bearer ${JSON.parse(localStorage.getItem("access"))}`,
},
})
const article = await data.json()
const text = (article.body.ru || article.body.en)
.replace(/!\[.*?\]\(.*?\)/g, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/[#*_`>-]/g, "")
.replace(/\n+/g, " ")
.trim()
const utterance = new SpeechSynthesisUtterance(text)
utterance.lang = article.body.ru ? "ru-RU" : "en-EN"
utterance.rate = 1
utterance.pitch = 1
utterance.volume = 1
speechSynthesis.cancel()
speechSynthesis.speak(utterance)
})
document.querySelector("#stop-reading").addEventListener("click", () => {
speechSynthesis.cancel()
})
</script>
Для чего: дать возможность выполнить расчеты не покидая приложение
Внешний вид:

Код:
<style>
#notes-widget {
background: #fff8a6;
border-radius: 10px;
padding: 12px;
box-shadow:
0 3px 10px rgba(0, 0, 0, .15);
}
#notes {
min-height: 220px;
max-height: 300px;
overflow-y: auto;
outline: none;
font-family: "Segoe Print", "Comic Sans MS", cursive;
font-size: 15px;
line-height: 30px;
padding: 8px 10px;
background-color: transparent;
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 29px,
rgba(0,0,0,.15) 30px
);
white-space: pre-wrap;
color: black;
}
#notes:empty::before {
content: "Напишите что-нибудь...";
color: rgba(0,0,0,.4);
}
#notes::-webkit-scrollbar {
display: none;
}
#status {
margin-top: 8px;
text-align: right;
font-size: 12px;
color: #666;
}
</style>
<div id="notes-widget">
<div id="notes" contenteditable="true"></div>
<div id="status"></div>
</div>
<script type="module">
const STORAGE_KEY = `quick-notes:{{ticket.id}}`
const notes = document.querySelector("#notes")
const status = document.querySelector("#status")
notes.innerHTML = localStorage.getItem(STORAGE_KEY) ?? ""
let saveTimer
let statusTimer
function showStatus(text) {
status.textContent = text
clearTimeout(statusTimer)
statusTimer = setTimeout(() => {
status.textContent = ""
}, 1500)
}
function save() {
localStorage.setItem(STORAGE_KEY, notes.innerHTML)
showStatus("Сохранено ✓")
}
notes.addEventListener("input", () => {
clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
save()
}, 500)
})
notes.addEventListener("keydown", event => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault()
save()
}
})
</script>