--- title: Компоненты фронтенда description: Создавайте компоненты React, которые отображаются внутри интерфейса Twenty в изолированной песочнице. icon: window-maximize --- Фронтенд-компоненты — это компоненты React, которые отображаются непосредственно внутри интерфейса Twenty. Они выполняются в **изолированном Web Worker** с использованием Remote DOM — ваш код исполняется внутри изолированного iframe с непрозрачным источником (opaque-origin), но его UI всё равно нативно рендерится на странице, а не ограничивается этим iframe. Конструкции передней части все еще активно развиваются. Ваш код запускается с частичным DOM, а не с реальной страницей браузера, так что использование расширенного кода может привести к ошибке, часто молчанию. См. [Текущие ограничения](#current-limitations). ## Где можно использовать фронт-компоненты Фронт-компоненты могут отображаться в трёх местах внутри Twenty: * **Боковая панель** — фронт-компоненты с интерфейсом открываются в правой боковой панели. Это поведение по умолчанию, когда фронт-компонент запускается из меню команд. * **Виджеты (дашборды и страницы записей)** — фронт-компоненты можно встраивать как виджеты в [макеты страниц](/l/ru/developers/extend/apps/layout/page-layouts). При настройке дашборда или макета страницы записи пользователи могут добавить виджет фронт-компонента. * **Настройки приложения (App settings)** — если определить фронт-компонент с помощью [`defineSettingsFrontComponent()`](#custom-settings-component), он будет отображаться как раздел на вкладке **Settings** приложения, заменяя стандартный интерфейс настройки переменных. Сам по себе фронт-компонент недоступен из интерфейса — его нужно *сделать доступным*. Сделать это можно тремя способами: * **Связать его с [элементом командного меню](/l/ru/developers/extend/apps/layout/command-menu-items)** — регистрирует его в командном меню (Cmd+K) и, при необходимости, как закреплённое быстрое действие. * **Встроить его как виджет в [макет страницы](/l/ru/developers/extend/apps/layout/page-layouts)** — размещает его на странице деталей записи или на дашборде. * **Определить его с помощью [`defineSettingsFrontComponent()`](#custom-settings-component)** — отображает его как раздел на вкладке **Settings** приложения, заменяя стандартный интерфейс настройки переменных. ## Простой пример Самый быстрый способ увидеть фронт-компонент в действии — связать его с [`defineCommandMenuItem`](/l/ru/developers/extend/apps/layout/command-menu-items), чтобы он появился как кнопка быстрого действия в правом верхнем углу страницы: ```tsx src/front-components/hello-world.tsx import { defineFrontComponent } from 'twenty-sdk/define'; const HelloWorld = () => { return (

Hello from my app!

This component renders inside Twenty.

); }; export default defineFrontComponent({ universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', name: 'hello-world', description: 'A simple front component', component: HelloWorld, }); ``` ```ts src/command-menu-items/hello-world.command-menu-item.ts import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', shortLabel: 'Hello', label: 'Hello World', isPinned: true, availabilityType: 'GLOBAL', frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', }); ``` После синхронизации с помощью `yarn twenty dev` (или однократного запуска `yarn twenty apply`) быстрое действие появится в правом верхнем углу страницы:
Кнопка быстрого действия в правом верхнем углу
Нажмите её, чтобы отобразить компонент инлайн. ## Поля конфигурации | Поле | Обязательно | Описание | | --------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `universalIdentifier` | Да | Стабильный уникальный идентификатор для этого компонента | | `component` | Да | Функция компонента React | | `name` | Нет | Отображаемое имя | | `description` | Нет | Описание того, что делает компонент | | `isHeadless` | Нет | Установите значение `true`, если у компонента нет видимого пользовательского интерфейса (см. ниже) | ## Размещение фронт-компонента на странице Помимо команд, вы можете встроить фронт-компонент непосредственно на страницу записи, добавив его как виджет в **макет страницы**. См. [макеты страниц](/l/ru/developers/extend/apps/layout/page-layouts) для подробностей. ## Пользовательский компонент настроек Чтобы заменить автоматически сгенерированный интерфейс настройки переменных на вкладке **Settings** вашего приложения собственным компонентом, определите его с помощью `defineSettingsFrontComponent` вместо `defineFrontComponent`. Он использует те же [поля конфигурации](#configuration-fields) (за исключением `isHeadless`, которое не принимается, поскольку компонент настроек всегда отрисовывает видимый интерфейс пользователя) и дополнительно помечает компонент как интерфейс настроек приложения. Компонент отрисовывается как раздел **внутри** вкладки Settings, а не как замена всей вкладки. Управляемые системой Twenty разделы — автообновление, App URL и подключения — всегда отрисовываются над ним и не могут быть переопределены приложением. ```tsx src/front-components/app-settings.tsx import { defineSettingsFrontComponent } from 'twenty-sdk/define'; const AppSettings = () => { return (

My app settings

{/* render your own configuration UI here */}
); }; export default defineSettingsFrontComponent({ universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', name: 'app-settings', description: "Custom UI for the app's Settings tab", component: AppSettings, }); ``` Для каждого приложения допускается только один фронт-компонент настроек; объявление более одного приведет к ошибке сборки. Если он присутствует, вкладка **Settings** приложения отрисовывает этот компонент вместо стандартного интерфейса конфигурации переменных. ## Headless и non-headless Фронт-компоненты поддерживают два режима отображения, управляемых опцией `isHeadless`: **Non-headless (по умолчанию)** — компонент отображает видимый интерфейс. При запуске из меню команд он открывается в боковой панели. Это поведение по умолчанию, когда `isHeadless` имеет значение `false` или опущен. **Headless (`isHeadless: true`)** — компонент монтируется невидимо в фоновом режиме. Он не открывает боковую панель. Компоненты headless предназначены для действий, которые выполняют логику и затем размонтируются — например, запуск асинхронной задачи, переход на страницу или показ модального окна подтверждения. Они естественно сочетаются с компонентами SDK Command, описанными ниже. ```tsx src/front-components/sync-tracker.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component'; import { useEffect } from 'react'; const SyncTracker = () => { const [recordId] = useSelectedRecordIds(); useEffect(() => { enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' }); }, [recordId]); return null; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'sync-tracker', description: 'Tracks record views silently', isHeadless: true, component: SyncTracker, }); ``` Поскольку компонент возвращает `null`, Twenty пропускает рендеринг контейнера для него — в макете не появляется пустое место. Компонент по-прежнему имеет доступ ко всем хукам и API взаимодействия с хостом. ## Компоненты SDK Command Пакет `twenty-sdk` предоставляет четыре вспомогательных компонента Command, предназначенных для headless фронт-компонентов. Каждый компонент выполняет действие при монтировании, обрабатывает ошибки, показывая уведомление snackbar, и автоматически размонтирует фронт-компонент по завершении. Импортируйте их из `twenty-sdk/front-component`: * **`Command`** — запускает асинхронный колбэк через проп `execute`. * **`CommandLink`** — переходит по пути внутри приложения. Пропы: `to`, `params`, `queryParams`, `options`. * **`CommandModal`** — открывает модальное окно подтверждения. Если пользователь подтвердит, выполняет колбэк `execute`. Пропы: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. * **`CommandOpenSidePanelPage`** — открывает страницу боковой панели. Пропсы зависят от `page` — например, `ViewRecord` принимает `recordId` + `objectNameSingular` (а также необязательный id `tab`, чтобы открыть запись на определённой вкладке), другие страницы принимают `pageTitle` + `pageIcon`. Полный пример headless фронт-компонента, использующего `Command` для запуска действия из меню команд: ```tsx src/front-components/run-action.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; const RunAction = () => { const execute = async () => { const client = new CoreApiClient(); await client.mutation({ createTask: { __args: { data: { title: 'Created by my app' } }, id: true, }, }); }; return ; }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', name: 'run-action', description: 'Creates a task from the command menu', component: RunAction, isHeadless: true, }); ``` ```ts src/command-menu-items/run-action.command-menu-item.ts import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', label: 'Run my action', frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', }); ``` А также пример с использованием `CommandModal` для запроса подтверждения перед выполнением: ```tsx src/front-components/delete-draft.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { CommandModal } from 'twenty-sdk/front-component'; const DeleteDraft = () => { const execute = async () => { // perform the deletion }; return ( ); }; export default defineFrontComponent({ universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', name: 'delete-draft', description: 'Deletes a draft with confirmation', component: DeleteDraft, isHeadless: true, }); ``` И пример использования `CommandOpenSidePanelPage` для открытия текущей записи в боковой панели на определённой вкладке. `tab` — это id вкладки в макете страницы (в стандартных макетах используются id вроде `company-tab-emails` или `company-tab-timeline`; в пользовательских макетах используется собственный id вкладки). Если такого id нет в макете записи, откроется вкладка по умолчанию: ```tsx src/front-components/open-company-emails.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { CommandOpenSidePanelPage, SidePanelPages, useSelectedRecordIds, } from 'twenty-sdk/front-component'; const OpenCompanyEmails = () => { const selectedRecordIds = useSelectedRecordIds(); const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null; if (!recordId) { return null; } return ( ); }; export default defineFrontComponent({ universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567', name: 'open-company-emails', description: 'Opens the current company on its Emails tab', component: OpenCompanyEmails, isHeadless: true, }); ``` ## Вызов логической функции Front-компоненты выполняются в браузере в Web Worker, изолированном внутри iframe с непрозрачным источником (opaque-origin), в то время как [логические функции](/l/ru/developers/extend/apps/logic/logic-functions) выполняются на стороне сервера. Между ними нет прямого внутрипроцессного вызова — вместо этого front-компонент обращается к логической функции по HTTP. Логическая функция, объявленная с `httpRouteTriggerSettings`, доступна по HTTP по своему пути маршрута. `RestApiClient` рассматривает пути, начинающиеся с `/s/`, как маршруты приложения, разрешает их в URL, по которому обслуживаются ваши функции, и аутентифицирует их с помощью `TWENTY_APP_ACCESS_TOKEN`. > **В Twenty Cloud логические функции с HTTP-триггером обслуживаются на выделенном домене для каждого рабочего пространства** по адресу `https://\.withtwenty.com\`. Для внешних вызовов скопируйте точный URL из настроек **HTTP trigger** функции или на вкладке **Settings** приложения. Безголовый front-компонент может выполнить вызов при монтировании через компонент `Command`, а затем автоматически размонтироваться: ```tsx src/front-components/sync-prs.tsx import { RestApiClient } from 'twenty-client-sdk/rest'; import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/front-component'; const SyncPrs = () => { const execute = async () => { await new RestApiClient().post('/s/github/fetch-prs', { owner: 'twentyhq', repo: 'twenty', }); }; return ; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'sync-prs', description: 'Triggers the fetch-prs logic function', isHeadless: true, component: SyncPrs, }); ``` Путь, передаваемый в `RestApiClient`, — это значение `httpRouteTriggerSettings.path` логической функции с префиксом `/s`. Сохраните `isAuthRequired: true`; `TWENTY_APP_ACCESS_TOKEN`, который Twenty выпускает для вашего компонента, аутентифицирует запрос: ```ts src/logic-functions/fetch-prs.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; import type { RoutePayload } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string }; // ...fetch from GitHub and persist records... return { ok: true }; }; export default defineLogicFunction({ universalIdentifier: '...', name: 'fetch-prs', handler, httpRouteTriggerSettings: { path: '/github/fetch-prs', httpMethod: 'POST', isAuthRequired: true, }, }); ``` `TWENTY_APP_ACCESS_TOKEN` внедряется автоматически — см. [переменные приложения](#application-variables). Поскольку секретные переменные приложения никогда не раскрываются front-компонентам, храните ключи API и другую конфиденциальную логику в логической функции, а не во front-компоненте. ### Вызов REST API Twenty Чтобы вызывать HTTP-маршруты приложения или читать и изменять записи Twenty из фронт-компонента, используйте `RestApiClient` из `twenty-client-sdk/rest`. Он отправляет пути вида `/s/...` на базовый URL функций вашего рабочего пространства, а все остальные пути, включая `/rest/...`, — на `TWENTY_API_URL`. Он всегда действует от имени человека, просматривающего страницу. `runAs: 'application'` — это параметр только для логической функции: компонент никогда не получает собственный токен вашего приложения, поэтому его запрос здесь вызывает ошибку. Поместите работу, требующую собственного доступа приложения, за логическую функцию и вызывайте вместо этого её. | Метод | Описание | | --------------------------------- | ----------------------------------------------------------------- | | `get(path, options?)` | Отправляет запрос `GET` | | `post(path, body?, options?)` | Отправляет запрос `POST` | | `put(path, body?, options?)` | Отправляет запрос `PUT` | | `patch(path, body?, options?)` | Отправляет запрос `PATCH` | | `delete(path, options?)` | Отправляет запрос `DELETE` | | `request(method, path, options?)` | Универсальный запрос с любым HTTP-методом | | `resolveUrl(path, options?)` | Разрешает путь в его полный URL без отправки запроса (для ссылок) | В `options` принимаются `headers`, `query` (объект с параметрами строки запроса; значения, равные null или undefined, пропускаются) и `AbortSignal` через `signal`. Объект `body`, не являющийся `FormData`, автоматически сериализуется в JSON. При получении `401` клиент один раз обновляет токен доступа через хост и повторяет запрос. Базовый URL и токен по умолчанию берутся из окружения. При необходимости передавайте переопределения в конструктор — например, в тестах: ```ts const client = new RestApiClient({ baseUrl: 'https://myworkspace.twenty.com', token: 'my-token', }); ``` Неудачные запросы выбрасывают `RestApiClientError`, который содержит `status`, `statusText`, `url` и распарсенное `body`: ```tsx import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest'; const client = new RestApiClient(); try { const people = await client.get('/rest/people', { query: { limit: 10 }, }); } catch (error) { if (error instanceof RestApiClientError) { console.error(error.status, error.body); } } ``` ## Доступ к контексту времени выполнения Внутри вашего компонента используйте хуки SDK для доступа к текущему пользователю, записи и экземпляру компонента: ```tsx src/front-components/record-info.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { useUserId, useSelectedRecordIds, useFrontComponentId, } from 'twenty-sdk/front-component'; const RecordInfo = () => { const userId = useUserId(); const [recordId] = useSelectedRecordIds(); const componentId = useFrontComponentId(); return (

User: {userId}

Record: {recordId ?? 'No record context'}

Component: {componentId}

); }; export default defineFrontComponent({ universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', name: 'record-info', component: RecordInfo, }); ``` Доступные хуки: | Хук | Возвращает | Описание | | --------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | | `useUserId()` | `string` или `null` | ID текущего пользователя | | `useSelectedRecordIds()` | `string[]` | Все выбранные идентификаторы записей (пустой массив, если ничего не выбрано) | | `useRecordId()` | `string` или `null` | **Устарело.** Используйте `useSelectedRecordIds()` вместо этого | | `useFrontComponentId()` | `string` | ID этого экземпляра компонента | | `useTimelineActivityId()` | `string` или `null` | Идентификатор текущего действия на временной шкале при рендеринге пользовательской строки временной шкалы | | `useColorScheme()` | `'light'` или `'dark'` | Активная цветовая схема интерфейса хоста (значение `System` уже определено) | | `useFrontComponentExecutionContext(selector)` | различается | Доступ к полному контексту выполнения с помощью функции-селектора | ## Переменные приложения Переменные приложения, определенные в [`defineApplication()`](/l/ru/developers/extend/apps/config/application) с `isSecret: false`, доступны внутри фронтенд-компонентов через утилиту `getApplicationVariable`: ```tsx src/front-components/greeting.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { getApplicationVariable } from 'twenty-sdk/front-component'; const Greeting = () => { const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World'; return

Hello, {recipientName}!

; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'greeting', component: Greeting, }); ``` Секретные переменные (`isSecret: true`) **не** доступны фронтенд-компонентам. Они доступны только в [логических функциях](/l/ru/developers/extend/apps/logic/logic-functions), которые выполняются на стороне сервера. Это предотвращает отправку в браузер конфиденциальных значений, таких как ключи API. `getApplicationVariable` всегда возвращает **строку** (или `undefined`), независимо от объявленного для переменной `type`. Строка сериализуется единообразно в зависимости от типа (логические значения как `"true"` / `"false"`, числа как десятичные строки, массивы / объекты как JSON) в том же формате, который используется для логической функции `process.env` — разбирайте её самостоятельно (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). См. [типы переменных](/l/ru/developers/extend/apps/config/application#variable-types). Следующие системные переменные всегда доступны через `process.env`: | Переменная | Описание | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TWENTY_API_URL` | Базовый URL основного API Twenty | | `TWENTY_APP_ACCESS_TOKEN` | Кратковременный токен, область действия которого ограничена пересечением роли вошедшего в систему пользователя и роли вашего приложения, поэтому компонент никогда не сможет сделать больше, чем человек, который его просматривает | ### `TWENTY_FUNCTIONS_URL` Twenty также внедряет `TWENTY_FUNCTIONS_URL` во фронт-компоненты и логические функции: это базовый URL, по которому обслуживаются HTTP-триггерные логические функции вашего приложения. Он существует, потому что этот URL не всегда совпадает с самим сервером Twenty. В Twenty Cloud маршруты приложения обслуживаются на выделенном домене для каждого рабочего пространства (`https://\.withtwenty.com` или основном общедоступном домене приложения, если он настроен), чтобы ответы, сформированные приложением, отдавались с изолированного источника, а не с источника приложения Twenty. Самостоятельно развёрнутые и локальные экземпляры обслуживают маршруты приложения с префиксом `/s` на самом сервере и могут вовсе не задавать эту переменную. Поскольку базовый URL различается для каждого рабочего пространства и экземпляра, ваш код не может жёстко прописать его — сервер внедряет правильное значение во время выполнения. Вам редко нужно читать его напрямую. Вызывайте свои маршруты через `RestApiClient` с путём, начинающимся с `/s/`, и клиент разрешит URL за вас: он убирает префикс `/s` и обращается к `TWENTY_FUNCTIONS_URL`, а если переменная не задана, использует `\/s`. Используйте `resolveUrl('/s/\')`, чтобы получить абсолютный URL без отправки запроса, например для ссылки. Читайте переменную напрямую только при ручной сборке URL: ```ts const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`; ``` ## API взаимодействия с хостом Компоненты фронтенда могут вызывать навигацию, модальные окна и уведомления с помощью функций из `twenty-sdk`: | Функция | Описание | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `navigate(to, params?, queryParams?, options?)` | Перейти на страницу в приложении | | `openSidePanelPage(params)` | Открыть боковую панель | | `closeSidePanel()` | Закрыть боковую панель | | `openCommandConfirmationModal(params)` | Показать диалог подтверждения | | `enqueueSnackbar(params)` | Показать всплывающее уведомление | | `unmountFrontComponent()` | Размонтировать компонент | | `updateProgress(progress)` | Обновить индикатор прогресса | | `uploadFile(file, { fieldMetadataId, fileName? })` | Загрузите `Blob` в поле FILES; возвращает `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` или `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` | Пример, который использует API хоста для показа snackbar и закрытия боковой панели после завершения действия: ```tsx src/front-components/archive-record.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-client-sdk/core'; const ArchiveRecord = () => { const [recordId] = useSelectedRecordIds(); const handleArchive = async () => { const client = new CoreApiClient(); await client.mutation({ updateTask: { __args: { id: recordId, data: { status: 'ARCHIVED' } }, id: true, }, }); await enqueueSnackbar({ message: 'Record archived', variant: 'success', }); await closeSidePanel(); }; return (

Archive this record?

); }; export default defineFrontComponent({ universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', name: 'archive-record', description: 'Archives the current record', component: ArchiveRecord, }); ``` ### Хранилище `localStorage` и `sessionStorage` работают так же, как на обычной странице, с использованием стандартного синхронного API. Ваши ключи ограничены рамками установки приложения и учетной записью вошедшего пользователя: ни одно другое приложение не может их прочитать, а другой пользователь, вошедший в тот же браузер, начинает с пустого хранилища. Значения, записанные в `localStorage`, остаются на устройстве при перезагрузках; `sessionStorage` действует в течение браузерной сессии. ```tsx src/front-components/note-draft.tsx import { useState } from 'react'; import { defineFrontComponent } from 'twenty-sdk/define'; const NoteDraft = () => { const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? ''); const handleChange = (nextDraft: string) => { setDraft(nextDraft); localStorage.setItem('draft', nextDraft); }; return (