---
metadata:
  - name: generator
    content: Diplodoc Platform v5.54.5
  - property: og:type
    content: article
  - property: article:section
    content: Вопросы о платформе плагинов
  - property: og:title
    content: Примеры
  - property: article:tag
    content: Техническая инструкция
alternate:
  - https://yandex.ru/support/tracker/en/plugins/examples.md
  - https://yandex.ru/support/tracker/ru/plugins/examples.md
  - href: ru/plugins/examples.md
    type: text/markdown
    title: Markdown version
  - href: ../llms.txt
    type: text/markdown
    title: llms.txt
---
> **Documentation Index:** Fetch the complete configuration index at https://yandex.ru/support/tracker/ru/llms.txt


# Вопросы о платформе плагинов

## Содержание {#toc}

- [Как перемещаться по интерфейсу?](#how-to-navigate)
- [У меня несколько слотов, так можно?](#several-slots)
- [А что такое контекст?](#what-is-context)
- [У меня запрос с пагинацией, как это сделать?](#pagination)
- [Где хранить настройки плагина?](#where-store-settings)
- [Как обращаться к внешним API?](#external-api)

### Как перемещаться по интерфейсу? {#how-to-navigate}

Плагин работает в iframe, поэтому переходы в интерфейс Трекера выполняются через `uiApi.navigate`. В React-пакете `uiApi` реэкспортируется из `@weavix/tracker-plugin-sdk-react`.

**Программный переход** — например, открыть очередь после импорта:

```tsx
import { uiApi } from "@weavix/tracker-plugin-sdk-react";

const openQueue = (queueKey: string) => {
    uiApi.navigate({
        path: `/${queueKey}`,
        options: { newTab: true },
    });
};
```

**Ссылки в разметке.** `TrackerPluginProvider` перехватывает клики по `<a href="...">` и элементам с `data-href`:

- относительные пути и ссылки на домен плагина открываются внутри iframe плагина;
- ссылки на Трекер и внешние сайты передаются приложению Трекера через `uiApi.navigate` (внешние — в новой вкладке).

```tsx
// Откроется в Трекере (в текущей или новой вкладке — по target)
<a href="/TREK-123">Перейти к задаче</a>
<a href="/TREK-123" target="_blank">Открыть в новой вкладке</a>

// Откроется внутри плагина (если путь относительный)
<a href="/settings">Настройки плагина</a>
```

Подробнее: [uiApi.navigate](https://yandex.ru/support/tracker/ru/plugins/tools/sdk/core.md#navigate).

### У меня несколько слотов, так можно? {#several-slots}

Да. В `manifest.json` можно указать несколько слотов — плагин появится в каждой точке интеграции. Обычно для всех слотов используют один `entrypoint` (`index.html`), а поведение в коде различают по полю `slot`.

```json
{
    "slots": {
        "tracker": {
            "navigation": [
                {
                    "entrypoint": "index.html",
                    "title": { "ru": "Мой отчет", "en": "My report" }
                }
            ],
            "issue.action": [
                {
                    "entrypoint": "index.html",
                    "title": {
                        "ru": "Действие с задачей",
                        "en": "Issue action"
                    }
                }
            ]
        }
    }
}
```

В коде — сужение по `slot`:

```tsx
import { useTrackerPluginContext } from "@weavix/tracker-plugin-sdk-react";

function App() {
    const { slot, slotContext, theme, language } = useTrackerPluginContext();

    if (slot === "navigation") {
        return <ReportPage theme={theme} language={language} />;
    }

    if (slot === "issue.action") {
        return <IssueAction issue={slotContext} />;
    }

    return null;
}
```

Список слотов и формат контекста для каждого — в разделе [Слоты](https://yandex.ru/support/tracker/ru/plugins/slots/index.md).

### А что такое контекст? {#what-is-context}

**Контекст запуска** — это данные, которые Трекер передает плагину при открытии. В React они доступны через `useTrackerPluginContext()`:

| Поле           | Что это                                                           |
| -------------- | ----------------------------------------------------------------- |
| `theme`        | Тема Трекера: `light`, `dark`, `system` и т.д.                    |
| `language`     | Язык интерфейса: `ru`, `en`                                       |
| `slot`         | Слот, из которого открыли плагин: `navigation`, `issue.action`, … |
| `slotContext`  | Данные окружения слота (формат зависит от уровня контекста)       |
| `contextLevel` | Уровень, объявленный в манифесте: `basic` или `full`              |

**Контекст слота** (`slotContext`) — данные со страницы, где открыли плагин. Объем данных задается полем **`contextLevel`** в конфигурации слота в `manifest.json` (обязательное поле). Уровень можно задать отдельно для каждого слота.

| Уровень                                     | В манифесте               | Что в `slotContext`                                                                                                      | Как получить в коде                                                |
| ------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| **`basic`** (по умолчанию в новых плагинах) | `"contextLevel": "basic"` | Только `{ entityId, entityMeta? }` — идентификатор сущности и опциональные метаданные из URL iframe, без запроса к Трекеру | `useTrackerPluginContext()` или `useTrackerPluginContext('basic')` |
| **`full`**                                  | `"contextLevel": "full"`  | Полный объект слота (`Issue`, контекст триггера и т.д.) — приложение Трекера передает данные через postMessage          | `useTrackerPluginContext('full')`                                  |

```json
{
    "slots": {
        "tracker": {
            "issue.action": [
                {
                    "entrypoint": "index.html",
                    "title": { "ru": "Мое действие", "en": "My action" },
                    "contextLevel": "full"
                }
            ]
        }
    }
}
```

**`basic`** — когда достаточно знать, _какую_ сущность открыли (ключ задачи в `entityId` или `entityMeta`), а поля загружаете сами через `trackerApi`. Быстрее старт плагина, меньше данных передает Трекер.

```tsx
import { useEffect, useState } from "react";
import {
    trackerApi,
    useTrackerPluginContext,
} from "@weavix/tracker-plugin-sdk-react";
import type { Issue } from "@weavix/tracker-api-types";

function IssueHeader() {
    const { slotContext } = useTrackerPluginContext<"issue.action">();
    const [issue, setIssue] = useState<Issue | null>(null);

    useEffect(() => {
        if (!slotContext?.entityId) return;
        trackerApi.v3.get["/issues/{id}"]({
            pathParams: { id: slotContext.entityId },
        }).then(({ data }) => setIssue(data));
    }, [slotContext?.entityId]);

    if (!issue) return null;
    return (
        <p>
            {issue.key}: {issue.summary}
        </p>
    );
}
```

**`full`** — когда нужны поля задачи сразу, без отдельного запроса. В манифесте обязательно `"contextLevel": "full"`, иначе при вызове `useTrackerPluginContext('full')` SDK выбросит ошибку.

```tsx
import { getField, useTrackerPluginContext } from "@weavix/tracker-plugin-sdk-react";

function IssueHeader() {
    const { slotContext } = useTrackerPluginContext<"issue.action">("full");

    if (!slotContext) return null;

    return (
        <p>
            {slotContext.key}: {getField(slotContext, "summary")}
        </p>
    );
}
```

При `full` тип `slotContext` совпадает с [публичным API](https://yandex.ru/support/tracker/ru/api-ref/about-api.md) для слота, например:

- `issue.action`, `issue.block`, `issue.tab` → `Issue`;
- `issue.comment.action` → данные комментария;
- `trigger.create.action` → ключ очереди;
- `navigation` → пустой объект `{}` (данные всё равно через `trackerApi`).

Полная таблица слотов — в разделе [Слоты](https://yandex.ru/support/tracker/ru/plugins/slots/index.md).

Тема и язык нужны, чтобы плагин выглядел как часть Трекера (`ThemeProvider`, `useLocalizedString`). Уровень `full` удобен на странице тикета, `basic` — когда нужен только идентификатор или вы и так загружаете данные через API.

### У меня запрос с пагинацией, как это сделать? {#pagination}

Через `trackerApi` плагин обращается к [публичному API Трекера](https://yandex.ru/support/tracker/ru/plugins/publicApi.md). Ответ приходит в виде `{ data, headers }` — заголовки нужны для пагинации.

Тип пагинации зависит от эндпоинта. Для поиска задач `POST /issues/_search` — см. [постраничное отображение](https://yandex.ru/support/tracker/ru/api-ref/issues/search-issues.md#pagination) и [относительную пагинацию](https://yandex.ru/support/tracker/ru/api-ref/issues/search-issues.md#relative-pagination).

**Постраничный поиск** (`filter`, `query` или `keys` в теле) — параметры `perPage` и `page`, в заголовках `X-Total-Count` и `X-Total-Pages`:

```tsx
import { useCallback, useState } from "react";
import { trackerApi } from "@weavix/tracker-plugin-sdk-react";
import type { Issue } from "@weavix/tracker-api-types";

function IssueList() {
    const [issues, setIssues] = useState<Issue[]>([]);
    const [page, setPage] = useState(1);
    const [totalPages, setTotalPages] = useState(1);
    const perPage = 20;

    const loadPage = useCallback(async (nextPage: number) => {
        const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
            queryParams: { perPage, page: nextPage },
            bodyParams: {
                filter: { assignee: "me", status: "open" },
            },
        });

        setIssues(data);
        setPage(nextPage);
        setTotalPages(Number(headers["x-total-pages"] ?? 1));
    }, []);

    return (
        <>
            <ul>
                {issues.map((issue) => (
                    <li key={issue.id}>{issue.key}</li>
                ))}
            </ul>
            <button disabled={page <= 1} onClick={() => loadPage(page - 1)}>
                Назад
            </button>
            <span>
                {page} / {totalPages}
            </span>
            <button
                disabled={page >= totalPages}
                onClick={() => loadPage(page + 1)}
            >
                Вперед
            </button>
        </>
    );
}
```

**Поиск по очереди** (`queue` в теле) — относительная пагинация: вместо `page` передается `id` из заголовка `Link` предыдущего ответа:

```tsx
const loadFirst = async () => {
    const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
        queryParams: { perPage: 20 },
        bodyParams: { queue: "TREK" },
    });
    setIssues(data);
    setNextPageId(parseNextId(headers.link)); // id из Link: ...; rel="next"
};

const loadNext = async (pageId: string) => {
    const { data, headers } = await trackerApi.v3.post["/issues/_search"]({
        queryParams: { perPage: 20, id: pageId },
        bodyParams: { queue: "TREK" },
    });
    setIssues((prev) => [...prev, ...data]);
    setNextPageId(parseNextId(headers.link));
};

function parseNextId(linkHeader?: string): string | null {
    if (!linkHeader) return null;
    const match = linkHeader.match(/[?&]id=([^&>]+)/);
    return match?.[1] ?? null;
}
```

Для больших выборок в `_search` также есть [прокрутка (scroll)](https://yandex.ru/support/tracker/ru/api-ref/issues/search-issues.md#scroll) — параметры `scrollType`, `scrollId`, `scrollToken` в `queryParams` (подсказки есть в автодополнении `trackerApi.v3.post['/issues/_search']`).

Добавьте в `manifest.json` нужные [разрешения](https://yandex.ru/support/tracker/ru/plugins/common.md#permissions), например `tracker:issues:read` для чтения задач.

### Где хранить настройки плагина? {#where-store-settings}

Для настроек уровня организации (общих для всех пользователей плагина в данной организации) используйте [`storageApi.orgShared`](https://yandex.ru/support/tracker/ru/plugins/storage.md). Это JSON-хранилище платформы — данные переживают перезагрузку и видны во всех вкладках.

Типичный сценарий — форма настроек плагина: загружаем текущее значение, отключаем редактирование, если у пользователя нет прав, сохраняем без явной версии (SDK сам повторит запрос при конфликтах).

```tsx
import { useCallback, useEffect, useState } from "react";
import {
    PluginActionError,
    storageApi,
    useToaster,
    VERSION_CONFLICT,
} from "@weavix/tracker-plugin-sdk-react";

type Settings = {
    autoReply: boolean;
    welcomeMessage: string;
};

const DEFAULTS: Settings = { autoReply: false, welcomeMessage: "" };

function SettingsForm() {
    const toaster = useToaster();
    const [settings, setSettings] = useState<Settings>(DEFAULTS);
    const [canWrite, setCanWrite] = useState(false);
    const [saving, setSaving] = useState(false);

    useEffect(() => {
        storageApi.orgShared.get("settings").then((record) => {
            if (!record) {
                // Записи еще нет — ее сможем создать
                setCanWrite(true);
                return;
            }
            setSettings({ ...DEFAULTS, ...(record.data as Settings) });
            setCanWrite(record.canWrite);
        });
    }, []);

    const handleSave = useCallback(async () => {
        setSaving(true);
        try {
            // version не передаем — SDK сам прочитает текущую и повторит запрос при конфликтах
            const updated = await storageApi.orgShared.patch({
                bucket: "settings",
                data: settings,
            });
            // patch возвращает объединенный результат (включая поля от параллельных процессов записи)
            setSettings({ ...DEFAULTS, ...(updated.data as Settings) });
            toaster.add({ title: "Настройки сохранены", theme: "success" });
        } catch (e) {
            if (e instanceof PluginActionError && e.code === VERSION_CONFLICT) {
                toaster.add({
                    title: "Не удалось сохранить",
                    theme: "danger",
                    content:
                        "Данные изменили в параллельной сессии. Перезагрузите форму.",
                });
            } else {
                throw e;
            }
        } finally {
            setSaving(false);
        }
    }, [settings, toaster]);

    if (!canWrite) {
        return <p>Только администратор плагина может менять эти настройки.</p>;
    }

    return (
        <form
            onSubmit={(e) => {
                e.preventDefault();
                handleSave();
            }}
        >
            <label>
                <input
                    type="checkbox"
                    checked={settings.autoReply}
                    onChange={(e) =>
                        setSettings({
                            ...settings,
                            autoReply: e.target.checked,
                        })
                    }
                />
                Автоответ на новые задачи
            </label>
            <textarea
                value={settings.welcomeMessage}
                onChange={(e) =>
                    setSettings({ ...settings, welcomeMessage: e.target.value })
                }
            />
            <button type="submit" disabled={saving}>
                Сохранить
            </button>
        </form>
    );
}
```

Если приложение само держит актуальную `version` (например, экран редактирования долго висит у пользователя), передавайте ее в `patch` явно — тогда `VERSION_CONFLICT` придет сразу, без ретраев, и можно показать пользователю «данные изменились, перечитайте». Подробнее — в разделе [Версионирование](https://yandex.ru/support/tracker/ru/plugins/storage.md#versioning).

### Как обращаться к внешним API? {#external-api}

Плагин работает в iframe с жесткой политикой CSP — прямые `fetch`/`XMLHttpRequest` к внешним сервисам браузер заблокирует. Все HTTP-запросы наружу **обязаны** идти через `hostApi.externalApiCall()`: плагин передает параметры запроса платформе через SDK, а прокси платформы выполняет запрос и возвращает результат. Домены, к которым обращается плагин, нужно заранее перечислить в `manifest.json` в секции `permissions.external`.

Подробнее о настройке манифеста, авторизации, обработке ошибок и всех методах `externalApi*` — в разделе [Внешние API](https://yandex.ru/support/tracker/ru/plugins/externalApi.md).

