Skip to main content
Фронтенд-компоненты — это компоненты React, которые отображаются непосредственно внутри интерфейса Twenty. Они выполняются в изолированном Web Worker с использованием Remote DOM — ваш код исполняется внутри изолированного iframe с непрозрачным источником (opaque-origin), но его UI всё равно нативно рендерится на странице, а не ограничивается этим iframe.
Конструкции передней части все еще активно развиваются. Ваш код запускается с частичным DOM, а не с реальной страницей браузера, так что использование расширенного кода может привести к ошибке, часто молчанию. См. Текущие ограничения.

Где можно использовать фронт-компоненты

Фронт-компоненты могут отображаться в трёх местах внутри Twenty:
  • Боковая панель — фронт-компоненты с интерфейсом открываются в правой боковой панели. Это поведение по умолчанию, когда фронт-компонент запускается из меню команд.
  • Виджеты (дашборды и страницы записей) — фронт-компоненты можно встраивать как виджеты в макеты страниц. При настройке дашборда или макета страницы записи пользователи могут добавить виджет фронт-компонента.
  • Настройки приложения (App settings) — если определить фронт-компонент с помощью defineSettingsFrontComponent(), он будет отображаться как раздел на вкладке Settings приложения, заменяя стандартный интерфейс настройки переменных.
Сам по себе фронт-компонент недоступен из интерфейса — его нужно сделать доступным. Сделать это можно тремя способами:
  • Связать его с элементом командного меню — регистрирует его в командном меню (Cmd+K) и, при необходимости, как закреплённое быстрое действие.
  • Встроить его как виджет в макет страницы — размещает его на странице деталей записи или на дашборде.
  • Определить его с помощью defineSettingsFrontComponent() — отображает его как раздел на вкладке Settings приложения, заменяя стандартный интерфейс настройки переменных.

Простой пример

Самый быстрый способ увидеть фронт-компонент в действии — связать его с defineCommandMenuItem, чтобы он появился как кнопка быстрого действия в правом верхнем углу страницы:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
После синхронизации с помощью yarn twenty dev (или однократного запуска yarn twenty apply) быстрое действие появится в правом верхнем углу страницы:
Нажмите её, чтобы отобразить компонент инлайн.

Поля конфигурации

Размещение фронт-компонента на странице

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

Пользовательский компонент настроек

Чтобы заменить автоматически сгенерированный интерфейс настройки переменных на вкладке Settings вашего приложения собственным компонентом, определите его с помощью defineSettingsFrontComponent вместо defineFrontComponent. Он использует те же поля конфигурации (за исключением isHeadless, которое не принимается, поскольку компонент настроек всегда отрисовывает видимый интерфейс пользователя) и дополнительно помечает компонент как интерфейс настроек приложения. Компонент отрисовывается как раздел внутри вкладки Settings, а не как замена всей вкладки. Управляемые системой Twenty разделы — автообновление, App URL и подключения — всегда отрисовываются над ним и не могут быть переопределены приложением.
src/front-components/app-settings.tsx
Для каждого приложения допускается только один фронт-компонент настроек; объявление более одного приведет к ошибке сборки. Если он присутствует, вкладка Settings приложения отрисовывает этот компонент вместо стандартного интерфейса конфигурации переменных.

Headless и non-headless

Фронт-компоненты поддерживают два режима отображения, управляемых опцией isHeadless: Non-headless (по умолчанию) — компонент отображает видимый интерфейс. При запуске из меню команд он открывается в боковой панели. Это поведение по умолчанию, когда isHeadless имеет значение false или опущен. Headless (isHeadless: true) — компонент монтируется невидимо в фоновом режиме. Он не открывает боковую панель. Компоненты headless предназначены для действий, которые выполняют логику и затем размонтируются — например, запуск асинхронной задачи, переход на страницу или показ модального окна подтверждения. Они естественно сочетаются с компонентами SDK Command, описанными ниже.
src/front-components/sync-tracker.tsx
Поскольку компонент возвращает 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 для запуска действия из меню команд:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
А также пример с использованием CommandModal для запроса подтверждения перед выполнением:
src/front-components/delete-draft.tsx
И пример использования CommandOpenSidePanelPage для открытия текущей записи в боковой панели на определённой вкладке. tab — это id вкладки в макете страницы (в стандартных макетах используются id вроде company-tab-emails или company-tab-timeline; в пользовательских макетах используется собственный id вкладки). Если такого id нет в макете записи, откроется вкладка по умолчанию:
src/front-components/open-company-emails.tsx

Вызов логической функции

Front-компоненты выполняются в браузере в Web Worker, изолированном внутри iframe с непрозрачным источником (opaque-origin), в то время как логические функции выполняются на стороне сервера. Между ними нет прямого внутрипроцессного вызова — вместо этого front-компонент обращается к логической функции по HTTP. Логическая функция, объявленная с httpRouteTriggerSettings, доступна по HTTP по своему пути маршрута. RestApiClient рассматривает пути, начинающиеся с /s/, как маршруты приложения, разрешает их в URL, по которому обслуживаются ваши функции, и аутентифицирует их с помощью TWENTY_APP_ACCESS_TOKEN.
В Twenty Cloud логические функции с HTTP-триггером обслуживаются на выделенном домене для каждого рабочего пространства по адресу https://\<your-workspace-subdomain>.withtwenty.com\<path>. Для внешних вызовов скопируйте точный URL из настроек HTTP trigger функции или на вкладке Settings приложения.
Безголовый front-компонент может выполнить вызов при монтировании через компонент Command, а затем автоматически размонтироваться:
src/front-components/sync-prs.tsx
Путь, передаваемый в RestApiClient, — это значение httpRouteTriggerSettings.path логической функции с префиксом /s. Сохраните isAuthRequired: true; TWENTY_APP_ACCESS_TOKEN, который Twenty выпускает для вашего компонента, аутентифицирует запрос:
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN внедряется автоматически — см. переменные приложения. Поскольку секретные переменные приложения никогда не раскрываются front-компонентам, храните ключи API и другую конфиденциальную логику в логической функции, а не во front-компоненте.

Вызов REST API Twenty

Чтобы вызывать HTTP-маршруты приложения или читать и изменять записи Twenty из фронт-компонента, используйте RestApiClient из twenty-client-sdk/rest. Он отправляет пути вида /s/... на базовый URL функций вашего рабочего пространства, а все остальные пути, включая /rest/..., — на TWENTY_API_URL. Он всегда действует от имени человека, просматривающего страницу. runAs: 'application' — это параметр только для логической функции: компонент никогда не получает собственный токен вашего приложения, поэтому его запрос здесь вызывает ошибку. Поместите работу, требующую собственного доступа приложения, за логическую функцию и вызывайте вместо этого её. В options принимаются headers, query (объект с параметрами строки запроса; значения, равные null или undefined, пропускаются) и AbortSignal через signal. Объект body, не являющийся FormData, автоматически сериализуется в JSON. При получении 401 клиент один раз обновляет токен доступа через хост и повторяет запрос. Базовый URL и токен по умолчанию берутся из окружения. При необходимости передавайте переопределения в конструктор — например, в тестах:
Неудачные запросы выбрасывают RestApiClientError, который содержит status, statusText, url и распарсенное body:

Доступ к контексту времени выполнения

Внутри вашего компонента используйте хуки SDK для доступа к текущему пользователю, записи и экземпляру компонента:
src/front-components/record-info.tsx
Доступные хуки:

Переменные приложения

Переменные приложения, определенные в defineApplication() с isSecret: false, доступны внутри фронтенд-компонентов через утилиту getApplicationVariable:
src/front-components/greeting.tsx
Секретные переменные (isSecret: true) не доступны фронтенд-компонентам. Они доступны только в логических функциях, которые выполняются на стороне сервера. Это предотвращает отправку в браузер конфиденциальных значений, таких как ключи API.
getApplicationVariable всегда возвращает строку (или undefined), независимо от объявленного для переменной type. Строка сериализуется единообразно в зависимости от типа (логические значения как "true" / "false", числа как десятичные строки, массивы / объекты как JSON) в том же формате, который используется для логической функции process.env — разбирайте её самостоятельно (Number(...), JSON.parse(...), === 'true'). См. типы переменных. Следующие системные переменные всегда доступны через process.env:

TWENTY_FUNCTIONS_URL

Twenty также внедряет TWENTY_FUNCTIONS_URL во фронт-компоненты и логические функции: это базовый URL, по которому обслуживаются HTTP-триггерные логические функции вашего приложения. Он существует, потому что этот URL не всегда совпадает с самим сервером Twenty. В Twenty Cloud маршруты приложения обслуживаются на выделенном домене для каждого рабочего пространства (https://\<your-workspace-subdomain>.withtwenty.com или основном общедоступном домене приложения, если он настроен), чтобы ответы, сформированные приложением, отдавались с изолированного источника, а не с источника приложения Twenty. Самостоятельно развёрнутые и локальные экземпляры обслуживают маршруты приложения с префиксом /s на самом сервере и могут вовсе не задавать эту переменную. Поскольку базовый URL различается для каждого рабочего пространства и экземпляра, ваш код не может жёстко прописать его — сервер внедряет правильное значение во время выполнения. Вам редко нужно читать его напрямую. Вызывайте свои маршруты через RestApiClient с путём, начинающимся с /s/, и клиент разрешит URL за вас: он убирает префикс /s и обращается к TWENTY_FUNCTIONS_URL, а если переменная не задана, использует \<TWENTY_API_URL>/s. Используйте resolveUrl('/s/\<path>'), чтобы получить абсолютный URL без отправки запроса, например для ссылки. Читайте переменную напрямую только при ручной сборке URL:

API взаимодействия с хостом

Компоненты фронтенда могут вызывать навигацию, модальные окна и уведомления с помощью функций из twenty-sdk: Пример, который использует API хоста для показа snackbar и закрытия боковой панели после завершения действия:
src/front-components/archive-record.tsx

Хранилище

localStorage и sessionStorage работают так же, как на обычной странице, с использованием стандартного синхронного API. Ваши ключи ограничены рамками установки приложения и учетной записью вошедшего пользователя: ни одно другое приложение не может их прочитать, а другой пользователь, вошедший в тот же браузер, начинает с пустого хранилища. Значения, записанные в localStorage, остаются на устройстве при перезагрузках; sessionStorage действует в течение браузерной сессии.
src/front-components/note-draft.tsx
Twenty хранит значения от имени вашего приложения, поэтому запись применяется локально сразу же и сохраняется в фоновом режиме. Операции чтения никогда не блокируются ожиданием хоста. Ничто не синхронизируется: значения не следуют за пользователем в другой браузер или на другой компьютер, поэтому используйте хранилище ключ-значение логической функции для всего, что должно пережить смену устройства. Записи ограничены, и для каждого лимита считаются символы, а не байты: ключи могут содержать не более 512 символов, одно значение — не более 262144 символов, а каждое хранилище — не более 1048576 символов на приложение и пользователя. Запись, нарушающая лимит, выбрасывает QuotaExceededError, как и браузерный API.

Работа с несколькими записями

Используйте useSelectedRecordIds() для обработки нескольких выбранных записей. Это полезно для массовых операций:
src/front-components/bulk-export.tsx
Сделайте его доступным через элемент командного меню, доступный только при выборе записей:
src/command-menu-items/bulk-export.command-menu-item.ts

Публичные ресурсы

Компоненты фронтенда могут получать доступ к файлам из каталога приложения public/ с помощью getPublicAssetUrl:
См. раздел о публичных ресурсах для подробностей.

Распространение зависимостей между фронт-компонентами

По умолчанию, каждая передняя часть объединяет свою собственную копию импортируемых библиотек, так что приложение с пятью компонентами поставляет React пять раз. Объявление общих зависимостей в пакете вашего приложения. son`, чтобы собрать эти библиотеки один раз и загрузить каждый компонент приложения из одного кэшированного файла:
package.json
Затем каждый компонент импортирует его зависимости точно так же, как и раньше: ничего не изменяется в коде компонента:
src/front-components/counter.tsx
Несколько важных моментов:
  • Один общий набор зависимостей для каждого приложения. Пакет построен из зависимостей вашего приложения, так что вы сохраняете полный контроль над версиями, которые вы поставляете.
  • Перечислите точные спецификаторы, которые вы импортируете. twenty-ui/input и twenty-ui/display являются двумя записями; только имя пакета не содержит подпусков. Список react автоматически охватывает react/jsx-runtime.
  • Поделиться react-dom/client вместе с react. Каждый компонент проделывает createRoot, поэтому оставив его, значит каждый компонент всё ещё объединяет React DOM.
  • **Набор кэширован. * Он обслуживается под content-hash URL с длительным неизменяемым кэшем, так что загружается один раз и повторно используется всеми компонентами приложения до тех пор, пока одна из зависимостей не изменится.
  • Компоненты, импортирующие ни один из общих пакетов никогда не загружают его.

Стилизация

Компоненты фронтенда поддерживают несколько подходов к стилизации. Вы можете использовать:
  • Встроенные стилиstyle={{ color: 'red' }}
  • Twenty UI components — собственная библиотека компонентов Twenty; см. раздел Using Twenty UI components ниже
  • Emotion — CSS-in-JS с @emotion/react
  • Styled-components — паттерны styled.div
  • Tailwind CSS — утилитарные классы
  • Любая библиотека CSS-in-JS, совместимая с React

Использование компонентов Twenty UI

Twenty поставляет свою библиотеку компонентов как пакет twenty-ui. Компоненты фронтенда могут использовать его для кнопок, тегов, статусных плашек, чипов, аватаров, иконок, типографики и токенов темы, которые автоматически соответствуют светлой и тёмной теме рабочего пространства.

Установка

Добавьте пакет в своё приложение, зафиксировав его на версии, с которой поставляется ваш экземпляр Twenty:
twenty-ui включается в ваш компонент фронтенда на этапе сборки, поэтому его достаточно иметь в зависимостях вашего приложения — во время выполнения ничего настраивать не нужно.

Импорт компонентов

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

Иконки

Импортируйте отдельные иконки из twenty-ui/icon:
Каждая именованная иконка участвует в tree-shaking, поэтому импорт нескольких иконок почти не увеличит размер вашего бандла. Избегайте IconsProvider, useIcons и iconsState — они подключают весь набор иконок Tabler (несколько мегабайт).

Темизация и токены темы

Компоненты Twenty UI автоматически подстраиваются под светлую и тёмную темы рабочего пространства — рендерер применяет активную цветовую схему на хосте, а компоненты вычисляют свои цвета относительно неё. Чтобы использовать те же дизайн‑токены в собственных встроенных стилях, вызовите хук useTheme(). Он возвращает токены темы Twenty (отступы, цвета, радиусы, шрифты), привязанные к активной теме, без необходимости настраивать ThemeProvider в вашем компоненте:
Поскольку useTheme() — это хук, вы читаете токены внутри тела компонента, поэтому значения всегда соответствуют активной теме. Та же карта токенов также экспортируется как константа themeCssVariables, но в компонентах фронтенда предпочтительнее использовать useTheme() — модульная константа, разыменующая themeCssVariables, может быть undefined, пока извлекается манифест приложения. Чтобы явно разветвлять логику по активной цветовой схеме, считайте её с помощью useColorScheme() из twenty-sdk/front-component, который возвращает 'light' или 'dark'.

Нынешние ограничения

Активно разрабатываются передние компоненты. Рендеринг, стилизация, обработка событий, измерение элементов и работа с хранилищем браузера работают хорошо. Все, что выходит за рамки этих возможностей (вызов DOM‑метода на ref, отслеживание изменения размеров элементов, порталирование за пределы вашего дерева), сегодня отсутствует или реализовано не полностью, и в большинстве случаев это тихо ломается: ни исключения, ни ошибки TypeScript, поскольку обвязка типизирована под полный DOM браузера. Если один из этих блоков вас, откройте задачу, чтобы получить приоритет.

Макет и измерение

Элементы могут измерять себя сами: хост отражает геометрию в песочницу, поэтому чтения выполняются локально, но могут отставать до одного кадра, а первое чтение никогда ранее не измеренного элемента возвращает нули. После записи выполните повторное чтение в колбэке requestAnimationFrame или в эффекте. Позиционирование с использованием getBoundingClientRect теперь работает, но всё, что отслеживает изменение размеров через ResizeObserver (recharts ResponsiveContainer, autoUpdate из Floating UI), по‑прежнему не работает. В любом случае отдавайте предпочтение CSS для верстки: ваша таблица стилей применяется к реальной странице, поэтому flexbox, grid, aspect-ratio, clamp() и @container работают как обычно, без задержки кадра.
requestAnimationFrame, fetch, setTimeout и queueMicrotask работают без префикса window.. Только window.requestAnimationFrame(...) и брошенные друзья.

DOM access

ref дает вам элемент песочницы, а не HTMLElement. Портал зазор поэтому всплывающие окна Radix, Headless UI, MUI и react-select не отображаются по умолчанию. Большинство из них принимают реквизиты контейнера; указывайте их на отображаемый элемент.

События

События мыши, указателя, касания, перетаскивания, клавиатуры, фокуса, input/change/submit, scroll/wheel/contextmenu и animationend/transitionend передаются хосту, плюс несколько на элемент: load/error на img, буфер обмена и композиция на input/textarea, медиа на video/audio, toggle на details/dialog. Все остальное (onAuxClick, onSelect, onInvalid, onReset, onAnimationStart, захват указателя, onLoad у img) отбрасывается без предупреждения. document.addEventListener() и window. ddEventListener() регистрируется без ошибок и никогда не стреляет, поэтому перетаскивание останавливается, как только указатель покидает начатый элемент. event.preventDefault() тоже не пересекается; форма представления, dragover/drop и ссылки уже охраняются для вас.

Атрибуты и стиль

Каждый элемент передает свои собственные свойства хостовому DOM (href на a, src/alt на img, value/placeholder/disabled на input и так далее), плюс общий набор на каждом элементе: id, className, style, title, tabIndex, role, draggable и любой атрибут aria-* / data-* (через дефис, поэтому ariaLabel отбрасывается). Всё, что за пределами тихо отбрасывается, поэтому выражать пользовательское состояние как data-*. CSS компонента, будь то из import './styles.css', CSS-in-JS или элемента style, внедряется в head хостовой страницы без области видимости. Таким образом, имена классов конфликтуют с собственным Twenty’s (префикс их), и никогда не пишут пустые div { ... } селекторы), а @media совпадает с вашим окном браузера вместо виджета (используйте @container с вашим собственным container-type). Встроенные свойства не затронуты.

Хранилище и сеть

localStorage и sessionStorage предоставляются Twenty, а не браузером: компонент выполняется в воркере с непрозрачным источником (origin), поэтому хост хранит значения от имени вашего приложения. См. раздел storage для получения информации об области действия и ограничениях. IndexedDB, cookies, Cache API и BroadcastChannel по-прежнему недоступны. Чтобы сохранять состояние между устройствами, вызовите логическую функцию и используйте ее хранилище ключ-значение. fetch работает с сохранениями:
  • Звонки к Twenty API и маршрутам вашего приложения проксируются узлом, поэтому предпочитайте RestApiClient. В случае проксируемых вызовов AbortSignal и другие опции RequestInit удаляются, и поддерживаются только тела string и URLSearchParams.
  • Другие источники оставляют песчаник с Origin: null, так что сторонний API отвечает, только если он посылает Access-Control-Allow-Origin: *. Вызовите его из логической функции.
  • fetch('/rest/people') никогда не совпадает с двадцать API, потому что песочница не имеет URL страницы для разрешения относительного пути.

Захват мультимедиа

navigator.mediaDevices.getUserMedia() и MediaRecorder работают внутри фронт-компонентов благодаря полифиллам песочницы, поэтому стандартный код записи выполняется без изменений, а MediaRecorder.isTypeSupported возвращает true для распространенных комбинаций контейнер/кодек. Подробные объекты ограничений getUserMedia принимаются, но не передаются дальше — хост выполняет захват с настройками по умолчанию для запрошенных типов — и одновременно во всех приложениях может быть активен только один захват. Сохраните записанный Blob с помощью хост-функции uploadFile.

Другие пробелы

  • Содержимое файла. Элемент input с типом file предоставляет вашему обработчику только метаданные файла, а не байты, поэтому FileReader недоступен. Чтобы загрузить Blob, который уже есть в вашем коде (например, созданный MediaRecorder), используйте хост-функцию uploadFile.
  • Перетащите приложения. Перетаскивайте события, но event.dataTransfer неопределённый.
  • Узел встроенный fs, path и node:crypto не срабатывают сборки, так что переместитесь с помощью logic функции. Веб-криптовалюты, fetch, TextEncoder и URL.
  • iframe всегда повторно изолируется без allow-same-origin, поэтому встраиваемый контент, зависящий от собственной сессии, отображается как неавторизованный. У него также нет onLoad.