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

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

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

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

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

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

Для того, чтобы подключить возможность управления веб-плагинами, необходимо обновить систему до [версии 6.0.x](https://support.swarmica.com/article/ru/1765-obnovlenie-v60x.html) или новее и добавить [фича-флаг](https://support.swarmica.com/article/ru/596-ficha-flagi-features-vozmozhnost-vklyuchat-i-vyklyuchat-bloki-funkcij-v-swarmica.html) `ui_widgets`.

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

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

1.  Зайдите в **Настройки > Интеграции > Кастомные виджеты**
2.  Нажмите **Создать** и выберите нужный раздел, в котором предполагается отображать виджет
3.  Задайте настройки виджета:
    1.  **Название** (будет отображаться в интерфейсе в качестве заголовка панели с виджетом)
    2.  **Доступен для** (указывает роли пользователей, которые будут видеть данный виджет)
    3.  **Код** (непосредственно код виджета. См. **Использование динамических данных контекста** про возможность динамической параметризации кода)
4.  Нажмите **Сохранить** и введите ID объекта в системе, чтобы посмотреть, как будет выглядеть виджет на странице этого объекта (например, номер заявки): <img height="357" width="703" alt="Пример настройки встраиваемого виджета веб-плагина в Swarmicaа" src="https://support.swarmica.com/attachments/4/p/4pjc8frue_ykFJyq_a/swarmica_web_plugin_configuration_example.png" />
5.  Чтобы включить отображение виджета, включите соответствующую настройку и нажмите **Сохранить**. Чтобы отрегулировать положение виджета на экране, зайдите в раздел **Настройки > Поля и формы > Настройки экранов** и переместите виджет в удобное для пользователей место: <img height="357" width="701" alt="Пример настройки отображения встраиваемого виджета веб-плагина в Swarmica" src="https://support.swarmica.com/attachments/5/f/5f-OjDSxDV6EjAnv_a/swarmica_web_plugin_layout_configuration.png" />

:::danger
**ВНИМАНИЕ!** Код виджета передается **в браузер всех пользователей системы**, которым, согласно настройкам, показывается виджет. **НЕ ИСПОЛЬЗУЙТЕ** перманентные токены для доступов API в коде виджета или любые авторизационные данные, которые могут позволить получить доступ к интегрируемым системам с повышенными привилегиями
:::

## Использование динамических данных контекста

При рендере (отображении) плагина для конечного пользователя системы, код проходит обработку шаблонизатора Jinja, куда в контекст передается объект, для которого данный плагин отображается. Таким образом, вы можете использовать любые атрибуты объекта, доступные в этом контексте.

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

```html
<iframe src="https://my_crm/accounts/{{ user.custom_fields.get_value('CF_UID') }}" />
```

Более подробно про переменные контекста можно узнать в статье ["Как использовать переменные контекста"](https://support.swarmica.com/article/ru/1614-kak-ispolzovat-peremennyekontekst-dlya-sozdaniya-skriptov-dlya-dejstviya-po-sobytiyu-v-swarmica.html).

## Примеры плагинов

Ниже примеры виджетов, которые демонстрируют возможности этого механизма

::::tabs
:::tab-item{title="Быстрые заметки"}
**Для чего**: дать возможность сотрудникам поддержки делать локальные (не сохраняющиеся на сервер) пометки при работе с заявкой. Например, при разговоре с клиентом по телефону.

**Внешний вид**:

<img height="477" width="699" src="https://support.swarmica.com/attachments/r/t/rtyjpkmi5f0EYYf0_a/swarmica_web_plugin_notes.png" />

**Код**:

```html
<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>
```
:::

:::tab-item{title="Карта"}
**Для чего**: отобразить адрес контрагента на карте

**Внешний вид**: <img height="405" width="690" src="https://support.swarmica.com/attachments/H/4/H4CdfQpRYAFgAkbn_a/swarmica_web_plugin_map.png" />

**Код**:

```html
<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>
```
:::

:::tab-item{title="Прочтение статьи вслух"}
**Для чего**: дать возможность редакторам проверить доступность статьи для людей с ограниченными возможностями

**Внешний вид**:

<img height="348" width="686" alt="Пример встраиваемого виджета веб-плагина в Swarmica: прочтение статьи вслух" src="https://support.swarmica.com/attachments/6/f/6f5Rz_E5jGbieh69_a/swarmica_web_plugin_read_aloud.png" />

**Код**:

```html
<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>
```
:::

:::tab-item{title="Калькулятор"}
**Для чего**: дать возможность выполнить расчеты не покидая приложение

**Внешний вид**:

<img height="342" width="670" src="https://support.swarmica.com/attachments/u/V/uVcEgCrmrauiHAU7_a/swarmica_web_plugin_calculator.png" />

**Код**:

```html
<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>
```
:::
::::