#1898: Собственные виджеты в Swarmica (веб-плагины)

Отредактирована: сегодня

Описание

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

Виджет, веб-плагин – это код HTML, который может использовать данные из контекста экрана (например, номер заявки, значение кастомного поля учетной записи пользователя и тд) в своей внутренней логике.

Примеры задач, которые решаются с помощью веб-плагинов:

  • Отобразить ссылку на учетную запись пользователя в CRM, используя значение поля user.ext_id
  • Отобразить в iframe раздел Личного Кабинета пользователя в приложении, динамически подставив в адрес iframe ID аккаунта
  • Отобразить мини-приложение для прочтения текста статьи вслух
  • Отобразить виджет для работы с лицензионным ключом в биллинге, для получения информации о лицензии и быстрых действий по продлению этого ключа

Веб-плагин может содержать любой код HTML, в том числе и блоки с Javascript и CSS, что позволяет реализовать практически любую логику любого frontend-приложения.

Для того, чтобы подключить возможность управления веб-плагинами, необходимо обновить систему до версии 6.0.x или новее и добавить фича-флаг ui_widgets.

Настройка веб-плагина

Чтобы добавить плагин в систему, выполните следующие действия:

  1. Зайдите в Настройки > Интеграции > Кастомные виджеты
  2. Нажмите Создать и выберите нужный раздел, в котором предполагается отображать виджет
  3. Задайте настройки виджета:
    1. Название (будет отображаться в интерфейсе в качестве заголовка панели с виджетом)
    2. Доступен для (указывает роли пользователей, которые будут видеть данный виджет)
    3. Код (непосредственно код виджета. См. Использование динамических данных контекста про возможность динамической параметризации кода)
  4. Нажмите Сохранить и введите ID объекта в системе, чтобы посмотреть, как будет выглядеть виджет на странице этого объекта (например, номер заявки): Пример настройки встраиваемого виджета веб-плагина в Swarmicaа
  5. Чтобы включить отображение виджета, включите соответствующую настройку и нажмите Сохранить. Чтобы отрегулировать положение виджета на экране, зайдите в раздел Настройки > Поля и формы > Настройки экранов и переместите виджет в удобное для пользователей место: Пример настройки отображения встраиваемого виджета веб-плагина в Swarmica

ВНИМАНИЕ! Код виджета передается в браузер всех пользователей системы, которым, согласно настройкам, показывается виджет. НЕ ИСПОЛЬЗУЙТЕ перманентные токены для доступов 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>

Для чего: дать возможность редакторам проверить доступность статьи для людей с ограниченными возможностями

Внешний вид:

Пример встраиваемого виджета веб-плагина в Swarmica: прочтение статьи вслух

Код:

<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>