Перейти к основному содержанию

Обзор

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

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

Фронт-компоненты могут отображаться в четырех местах внутри Twenty:
  • Боковая панель — фронт-компоненты с интерфейсом открываются в правой боковой панели. Это поведение по умолчанию, когда фронт-компонент запускается из меню команд.
  • Виджеты (дашборды и страницы записей) — фронт-компоненты можно встраивать как виджеты в макеты страниц. При настройке дашборда или макета страницы записи пользователи могут добавить виджет фронт-компонента.
  • Настройки приложения — на неё указывает объявление defineSettingsMenuItem(), а фронтенд-компонент отображает страницу за одним из пунктов меню настроек приложения. Приложение может объявить несколько таких компонентов.
  • Чат AI — Логическая функция, предоставленная как инструмент AI может задать фронт-компонент по имени, после чего он отображает вызовы этого инструмента непосредственно в чате вместо строки шага по умолчанию.
Сам по себе фронт-компонент недоступен из интерфейса — его нужно сделать доступным. Сделать это можно четырьмя способами:

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

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

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

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

Помимо команд, вы можете встроить фронт-компонент непосредственно на страницу записи, добавив его как виджет в макет страницы. См. макеты страниц для подробностей. widgetAvailability определяет, какое средство выбора виджетов предлагает компонент во время редактирования макета страницы. Фронтальный компонент создаётся либо для страниц записей, где он работает с отображаемой записью, либо для дашбордов, поэтому он объявляет один pageLayoutType:
Не указывайте widgetAvailability для компонента, который открывается только из команды, пункта меню настроек или строки временной шкалы. Он никогда не ограничивает место отображения компонента: команды, пункты меню настроек и макеты страниц, объявляемые вашим приложением, отображают его независимо от его значения. Сборка выдаёт предупреждение о видимом компоненте, который ничего не объявляет и не открывается ниоткуда больше.

Пункты меню настроек

Приложение может добавлять собственные пункты в меню настроек, каждый из которых отображает фронтенд-компонент. Объявите пункт с помощью defineSettingsMenuItem() в отдельном файле, указав компонент, который его отображает:
src/settings-menu-items/sync-settings.ts
Пункт меню указывает на фронтенд-компонент, а не на компонент, который объявляет себя пунктом настроек. Поэтому один компонент может обслуживать несколько пунктов меню, а пункт можно перенаправить на другой компонент, не изменяя ни один из них. Каждый пункт отображается как вкладка на странице приложения в разделе Настройки > Приложения, после Общие, в порядке position. Объявите столько, сколько нужно. Собственный элемент Twenty Общие — автоматическое обновление, URL приложения и подключения — всегда отображается первым и не может быть переопределен приложением; URL приложения отображается, только если у приложения есть функции, запускаемые по HTTP.

Поля

position — десятичное число, поэтому пункт можно разместить между двумя существующими без перенумерации. Пункты сортируются по возрастанию position; при совпадении используется universalIdentifier, поэтому порядок одинаков во всех рабочих пространствах. Два пункта одного приложения не могут иметь одинаковую позицию в одной области видимости — синхронизация отклонит это, а не позволит порядку установки определять, какой из них отобразится первым.

URL-адреса вкладок

Вкладка каждого элемента адресуется хешем в URL страницы, поэтому на нее можно ссылаться напрямую, например /settings/applications/\<applicationId>#sync или /settings/app-preferences/\<applicationId>#sync. Задайте slug, чтобы выбрать этот хеш; без него вкладка адресуется через universalIdentifier элемента. Добавление slug позднее сохраняет работоспособность ссылок, использующих universalIdentifier: они открывают ту же вкладку. Slug имеет формат kebab-case в нижнем регистре (буквы, цифры и одиночные дефисы, например sync или sync-settings) и содержит не более 64 символов. general и variables зарезервированы для встроенных вкладок, slug в формате UUID отклоняется, поскольку он может перекрыть ссылку на другую вкладку, а два элемента одного приложения не могут использовать один и тот же slug, даже в разных областях. Сборка и синхронизация отклоняют slug, нарушающий эти правила. После того как участник подключает или повторно подключает учетную запись в Настройки > Настройки приложения, он попадает на первую вкладку элемента USER приложения в порядке position или на Общие, если приложение не объявляет ни одной.

Область видимости рабочего пространства и пользователя

scope указывает, кем настраивается пункт:
  • WORKSPACE (по умолчанию) — настраивается один раз для всего рабочего пространства вместе с другими настройками рабочего пространства. Именно эти пункты отображаются как вкладки на странице приложения.
  • USER — настраивается каждым участником для себя в его собственных пользовательских настройках.
Элемент USER отображается как вкладка на странице приложения в разделе Настройки > Настройки приложения, где его видит только вошедший участник и только если его роль предоставляет разрешение Пользовательские приложения. Эта страница пока включена не в каждой рабочей области: там, где она не включена, элемент USER сохраняется и синхронизируется, но ничто его не отображает.
Пункт USER выполняется от имени участника, который его просматривает. Чтобы хранить данные для каждого пользователя, объявите переменную с scope: 'USER': getApplicationVariable возвращает значение для просматривающего участника, а ваш компонент сохраняет его с помощью updateMyUserApplicationVariable, который записывает значение только этого участника и требует SystemPermissionFlag.USER_APPS в роли вашего приложения, если она объявлена (см. Чтение и обновление переменных во время выполнения). Как и элемент WORKSPACE, элемент USER заменяет созданную форму Переменные, поэтому он должен позволять участнику задавать все пользовательские переменные, необходимые вашему приложению. Переменная рабочей области хранит одно значение, общее для всех, поэтому сохранение одним участником перезапишет значение другого.
defineSettingsFrontComponent() устарела в пользу defineSettingsMenuItem(). Приложение, всё ещё использующее её, продолжает работать — его компонент рассматривается как единственный пункт меню Настройки — однако оно может объявить только один такой пункт, и именно это ограничение снимает defineSettingsMenuItem().

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 — открывает содержимое в боковой панели. Страницы с маршрутами используют те же типизированные свойства to, params и queryParams, что и CommandLink, а также необязательные свойства hash, pageTitle и resetNavigationStack. Реестр маршрутов рабочей области определяет, поддерживает ли запрошенный AppPath боковую панель. pageTitle переопределяет заголовок в шапке панели, а resetNavigationStack очищает ранее открытые страницы панели перед открытием маршрута. Специализированные инструменты панели используют собственные свойства, специфичные для страниц.
Полный пример 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 для открытия текущей записи в боковой панели на определённой вкладке. hash — это 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:

Отправка во входящие

sendInboxMessage и вспомогательные функции упоминаний входящих также экспортируются из twenty-sdk/front-component, поэтому компонент может отправить сообщение без промежуточной логической функции. Сообщение отправлено из вашего приложения, а вызов содержит токен компонента, поэтому участнику, просматривающему страницу, как и каждому получателю, требуется разрешение AI. Чтобы затем показать беседу, перейдите к ней с помощью threadId, который возвращает вызов:
  • workspaceMemberIds принимает идентификаторы участников рабочего пространства, а не идентификатор пользователя, который возвращает useUserId().
  • Беседа открывается только для её участников, поэтому переходите к ней, когда участник, просматривающий страницу, является одним из получателей.
  • Входящие — это маршрут главной страницы: navigate открывает их там, а openSidePanelPage не может открыть их на боковой панели.

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

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

Метаданные объекта.

Используйте useSelectedObjectMetadata() для определения объекта, связанного с виджетом страницы записи, пользовательской строкой временной шкалы, командой в области объекта или фронт-компонентом, открытым в боковой панели с объектом (из команды или через openSidePanelPage с objectNameSingular). Это позволяет интеграции выбирать шаблоны или другую конфигурацию для текущего объекта без поиска идентификатора записи среди объектов.
Результат содержит id (UUID метаданных объекта рабочего пространства), nameSingular и namePlural. Имена являются именами метаданных, например person и people, а не отображаемыми метками. Метаданные объекта могут быть доступны, даже если не выбраны записи. Они принадлежат контексту этого фронт-компонента, поэтому боковая панель может предоставлять другой объект, отличный от объекта на основной странице. Хук обновляется при изменении этого контекста и возвращает null, когда объект невозможно разрешить, когда контекст объекта не предоставлен или при запуске на более старом хосте, который не предоставляет метаданные объекта.

Местоположение хоста

Используйте useLocation(), чтобы прочитать маршрут, на котором размещён ваш фронт-компонент. Он возвращает объект FrontComponentLocation с четырьмя обязательными строковыми полями: pathname, search, hash и origin. Например, страница по адресу https://myworkspace.twenty.com/page/page-layout-id?project=abc#details предоставляет соответственно /page/page-layout-id, ?project=abc, #details и https://myworkspace.twenty.com. hash включает начальный символ # и имеет значение '', когда фрагмент отсутствует. pathname, search и hash соответствуют контексту React Router компонента, включая собственный маршрут боковой панели, открытой через маршрутизацию. origin поступает из приложения-хоста. Хук обновляется при изменении маршрута. Старые хосты могут не включать executionContext.location, поэтому useLocation() возвращает null, когда местоположение недоступно. Для параметров запроса прочитайте useLocation()?.search ?? '' и разберите результат с помощью URLSearchParams. search включает начальный символ ? и имеет значение '', когда маршрут не содержит строки запроса. Резервное значение также предоставляет '', когда хост не предоставляет местоположение.

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

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

Чтение и обновление переменных во время выполнения

getApplicationVariable возвращает значения, внедренные при загрузке компонента, поэтому записанное позднее значение появится только при следующей загрузке. Чтобы прочитать сохранённые значения или записать значение (что делает пункт меню настроек, когда сохраняет введённое пользователем), используйте MetadataApiClient:
src/front-components/app-settings.tsx
Заметки:
  • Токен вашего компонента привязан к вашему собственному приложению. updateOneApplicationVariable нацелен на него, поэтому applicationId необязателен, а указание другого приложения отклоняется. Чтение applicationVariables другого приложения отклоняется таким же образом, а приложение из другого рабочего пространства при чтении считается не найденным.
  • Ключ должен быть объявлен вашим приложением в defineApplication(). Запись необъявленного ключа приводит к ошибке «не найдено».
  • Секретные переменные при чтении возвращаются замаскированными, а не в виде сохранённого значения. Их запись работает как обычно.
  • Для чтения и записи value переменной требуется разрешение настроек приложений рабочего пространства как для роли вашего приложения, так и для роли вошедшего пользователя, поскольку токен компонента является пересечением этих двух ролей. Добавьте SystemPermissionFlag.WORKSPACE_APPS в permissionFlagUniversalIdentifiers в вашей роли приложения.
  • В пользовательскую переменную (scope: 'USER') подставляется значение просматривающего участника, если она не является секретной. updateOneApplicationVariable отклоняет её: сохраните её с помощью updateMyUserApplicationVariable, как это делает saveMyLanguage выше. Она записывает только значение просматривающего участника и не позволяет указать другое приложение. Разрешение Приложения рабочего пространства не требуется, но разрешение Пользовательские приложения требуется для роли просматривающего участника и для роли вашего приложения, если она объявлена: добавьте SystemPermissionFlag.USER_APPS в permissionFlagUniversalIdentifiers в вашей роли приложения. См. Переменные рабочей области и пользователя.

Системные переменные

Следующие системные переменные всегда доступны через 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/front-component: Используйте openCommandConfirmationModal из twenty-sdk/front-component для диалогов. Принимает структурированные параметры заголовка, подзаголовка и кнопок и возвращает 'confirm' или 'cancel'. Twenty управляет модальным окном, управлением фокусом, закрытием и прокруткой страницы. Компоненты фронтенда не должны напрямую рендерить Dialog, AlertDialog, нативные элементы dialog или нативные всплывающие окна браузера. Передача локального контейнера портала не делает поддерживаемой прямую модальность, управляемую приложением. Пример, который использует 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

Выбрать все

Когда пользователь выбирает все записи представления, useSelectedRecordIds() возвращает пустой массив: выбор определяется фильтрами представления, поиском и неотмеченными строками и может включать гораздо больше записей, чем помещается в памяти. useSelectedRecordsFilter() в любом случае возвращает фильтр, точно соответствующий выбранным записям, поэтому выполняйте запрос с его помощью вместо перечисления идентификаторов:
Для больших выборок передайте фильтр в функцию логики и обрабатывайте там записи постранично.

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

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

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

По умолчанию, каждая передняя часть объединяет свою собственную копию импортируемых библиотек, так что приложение с пятью компонентами поставляет React пять раз. Объявление общих зависимостей в пакете вашего приложения. son`, чтобы собрать эти библиотеки один раз и загрузить каждый компонент приложения из одного кэшированного файла:
package.json
Затем каждый компонент импортирует его зависимости точно так же, как и раньше: ничего не изменяется в коде компонента:
src/front-components/counter.tsx
Несколько важных моментов:
  • Один общий набор зависимостей для каждого приложения. Пакет построен из зависимостей вашего приложения, так что вы сохраняете полный контроль над версиями, которые вы поставляете.
  • Перечислите точные спецификаторы, которые вы импортируете. twenty-ui/primitives/input и twenty-ui/primitives/data-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. Используйте его элементы управления и токены темы, чтобы придать front-компонентам такой же внешний вид и взаимодействия, как и остальной части Twenty. Ознакомьтесь с общими компонентами и примитивами, чтобы увидеть примеры, props и API композиции.

Установка

Новые приложения, созданные с помощью twenty app init, уже включают twenty-ui. Для существующего приложения добавьте его как зависимость для разработки, зафиксировав ту же версию, что и у twenty-sdk и twenty-client-sdk. Например, если ваши пакеты SDK используют версию 2.51.0:
twenty-ui включается во время сборки либо в каждый front-компонент, либо в пакет общих зависимостей вашего приложения. Twenty загружает таблицы стилей компонентов и темы и применяет активную цветовую схему рабочего пространства. Вам не нужно импортировать twenty-ui/style.css, таблицы стилей темы или добавлять корневой ThemeProvider, как в автономном приложении React.

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

Импортируйте из соответствующего общедоступного подпути семейства, чтобы зависимости оставались целевыми: Базовые элементы управления сгруппированы в twenty-ui/primitives/\<family>. Общие пресеты дизайна и строительные блоки приложения сгруппированы в twenty-ui/components/\<family>. Агрегированная точка входа twenty-ui/components по-прежнему доступна, но импорты семейств упрощают совместное использование только тех зависимостей, которые вы используете. Используйте MainButton для основных действий и LightButton для второстепенных действий. Используйте Button, Field, Input, Select и другие примитивы из twenty-ui/primitives/input для базовых элементов управления и форм. Следуйте документации по частям каждого составного элемента управления, таким как Field.Root, Field.Label и Field.Description. Передайте alignItemWithTrigger={false} в Select.Positioner. Выравнивание выбранного элемента по триггеру, используемое в Base UI по умолчанию, требует синхронных измерений макета, которые песочница не может предоставить, поэтому всплывающее окно сворачивается при повторном открытии. Используйте Dropdown из twenty-ui/components/navigation для меню действий, средств выбора параметров и немодальных панелей форм. Используйте enqueueSnackbar из twenty-sdk/front-component для уведомлений хоста. Автономные API Dialog и AlertDialog также экспортируются Twenty UI, но их прямое использование в компонентах фронтенда запрещено. Используйте API связи с хостом для диалогов. Этим точкам входа не требуется Monaco. Отдельная точка входа twenty-ui/components/code-editor зависит от Monaco, чей загрузчик скриптов не поддерживается в песочнице фронт-компонентов, поэтому CodeEditor не может стать там интерактивным.

Иконки

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

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

Компоненты Twenty UI автоматически соответствуют светлой и темной теме рабочего пространства. Средство рендеринга применяет активную цветовую схему на хосте, а компоненты разрешают относительно нее свои переменные CSS. Чтобы использовать те же токены дизайна в собственных встроенных стилях, вызовите useTheme() внутри тела компонента. Во фронт-компонентах по умолчанию он возвращает ссылки на переменные CSS, поэтому стили следуют активной теме хоста без локального ThemeProvider:
Те же ссылки CSS экспортируются как themeCssVariables из twenty-ui/theme. Считывайте их внутри тела компонента: CLI имитирует twenty-ui во время извлечения манифеста, поэтому разыменование импортированных констант темы на уровне модуля может привести к сбою сборки. Строки ссылок CSS не являются разрешенными числовыми значениями JavaScript, даже для токенов, таких как theme.icon.size.md. Не выполняйте над ними арифметические операции и не передавайте их API, которым требуются числа. См. Темы и Токены для семантических путей и имен переменных CSS. Чтобы явно разветвлять логику по активной цветовой схеме, считайте её с помощью useColorScheme() из twenty-sdk/front-component, который возвращает 'light' или 'dark'.

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

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

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

Элементы могут измерять себя сами: хост отражает геометрию в песочницу, поэтому чтения выполняются локально, но могут отставать до одного кадра, а первое чтение никогда ранее не измеренного элемента возвращает нули. После записи выполните повторное чтение в колбэке requestAnimationFrame или в эффекте. Позиционирование на основе getBoundingClientRect и отслеживание размера через ResizeObserver работают через одно и то же зеркало, поэтому обратные вызовы изменения размера отстают от хоста примерно на один кадр. В любом случае отдавайте предпочтение CSS для верстки: ваша таблица стилей применяется к реальной странице, поэтому flexbox, grid, aspect-ratio, clamp() и @container работают как обычно, без задержки кадра. matchMedia() вычисляет width, height, orientation, -webkit-device-pixel-ratio, resolution, hover, pointer и prefers-color-scheme в форме min-/max-, форме диапазона уровня 4 ((width >= 600px), (400px \<= width \<= 800px)) и форме без значения ((width)). Ширина, высота и orientation описывают блок вашего виджета, а не область просмотра браузера, поэтому они соответствуют document.body.clientWidth, а не window.innerWidth, а значения em и rem считаются равными 16px. hover и pointer описывают основной способ ввода на устройстве пользователя. Любое другое условие считается неизвестным, как браузеры обрабатывают неизвестные функции: элемент, разделённый запятыми, в котором оно встречается, вычисляется как false даже при not, если остальная часть этого элемента уже не определяет результат, как в not print and (prefers-reduced-motion: reduce). Это относится к неизвестным функциям и синтаксису, который этот парсер не реализует внутри условия, например к вложенным группам, таким как ((min-width: 600px)), not внутри группы и значениям calc(). or между условиями делает его элемент равным false. Конструкторы MediaQueryList и MediaQueryListEvent также недоступны, поэтому проверки instanceof с ними вызывают ReferenceError. Поскольку эта таблица стилей влияет на реальную страницу, правило @media (min-width: ...) вычисляется хостом относительно области просмотра браузера, тогда как matchMedia('(min-width: ...)') отвечает для блока вашего виджета. Чтобы CSS использовал тот же блок, который видит ваш JavaScript, объявите собственный container-type в обёртке и выполняйте запрос к нему: правило @container без него разрешается относительно ближайшего контейнера хоста, а не вашего виджета. То же различие относится к цветовой схеме: @media (prefers-color-scheme: dark) в таблице стилей следует настройкам браузера и ОС, тогда как matchMedia('(prefers-color-scheme: dark)') следует теме, выбранной в Twenty, поэтому, когда они должны совпадать, задавайте стили с помощью токенов useTheme(). Получаемая вами высота также относится к вашему собственному блоку, и в зависимости от места, куда вас монтирует хост, размер этого блока может определяться вашим собственным содержимым. Разветвление макета на основе min-height, max-height или orientation может затем влиять само на себя и колебаться. Для решений о макете предпочтительнее использовать запросы на основе ширины или @container с container-type: inline-size.
requestAnimationFrame работает как с префиксом window., так и без него. Только Chrome запускает настоящий кадр анимации внутри worker; Firefox и Safari получают таймер на 16 мс, поэтому используйте его, чтобы отложить повторное чтение после записи, а не для управления анимацией.

Прокрутка

Присваивание scrollTop или scrollLeft, вызов scrollTo() или его псевдонима scroll(), а также вызов scrollIntoView() прокручивают соответствующий элемент на странице:
  • Изменения прокрутки применяются после следующей отрисовки в том порядке, в котором их выполняет ваш код. В рамках одной отрисовки сначала применяются изменения выделения ввода, поэтому вызов setSelectionRange() с последующей записью в scrollLeft сохраняет каретку видимой.
  • Чтение возвращает записанное вами значение, пока страница не сообщит фактическую позицию, которую браузер ограничивает диапазоном прокрутки. В контейнерах с направлением справа налево scrollLeft равен нулю или отрицателен, как в браузерах.
  • scrollIntoView() принимает block и inline со значениями start, center, end и nearest, выравнивая block по вертикали, а inline по горизонтали, поэтому режимы вертикального письма не сопоставляются. Прокручиваются только собственные контейнеры прокрутки вашего компонента, но никогда не фрейм виджета, страница или другой компонент. focus() по-прежнему отображает сфокусированный элемент так, как это делает браузер.
  • Прокрутка выполняется мгновенно: behavior и параметр container игнорируются, а scrollBy() недоступен.
  • Повторная запись только что прочитанной позиции, например element.scrollTop = element.scrollTop, ничего не делает, поэтому она не может отменить прокрутку, выполненную пользователем за это время. Это верно только после того, как страница измерила элемент; до этого запись применяется.
  • Прокрутка document.documentElement, document.body или window остаётся внутри песочницы и никогда не прокручивает страницу; window.scrollTo(), scroll() и scrollBy() ничего не делают.
  • Первое чтение контейнера, который никогда не измерялся, возвращает нули (см. «Макет и измерение»), поэтому проверка возможности прокрутки, выполненная сразу после появления контейнера, может сообщить, что его нельзя прокручивать.

Глобальная область видимости

Собственные встроенные функции воркера также имеют псевдонимы в window, поэтому они работают как с префиксом window., так и без него: fetch, setTimeout/clearTimeout, setInterval/clearInterval, queueMicrotask, console, crypto, performance, structuredClone, btoa/atob, URL/URLSearchParams, AbortController/AbortSignal, Headers/Request/Response, Blob, File, FormData и TextEncoder/TextDecoder.

DOM access

ref дает вам элемент песочницы, а не HTMLElement. element.classList работает на основе атрибута class: add, remove, toggle, replace, contains, item, length, value и итерация ведут себя так же, как в браузере, а className остаётся синхронизированным. element.dataset работает так же, опираясь на атрибуты data-*; записывайте имена data-* в нижнем регистре, так как песочница сохраняет имена атрибутов в исходном виде, а не приводит их к нижнему регистру, как браузер. Порталы body могут выходить за пределы каждой границы вашего компонента на расстояние до 200px, в том числе за пределы области прокрутки виджета. Более крупные оверлеи обрезаются по границам этой области, поэтому добавьте длинным меню собственный контейнер с прокруткой. Навигация с клавиатуры в списках Twenty UI, таких как Autocomplete и Select, отображает выделенный вариант внутри этого контейнера. Нативные API верхнего слоя, такие как showModal(), showPopover() и requestFullscreen(), заблокированы.

События

События мыши, указателя, касания, перетаскивания, клавиатуры, фокуса, 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) отбрасывается без предупреждения. Обработчики получают типизированные события, созданные на основе события страницы: PointerEvent, MouseEvent, KeyboardEvent, InputEvent, WheelEvent или FocusEvent; эти конструкторы доступны в песочнице для dispatchEvent(). Каждое перенаправленное событие также содержит интерфейс синтетического события React (nativeEvent, isDefaultPrevented(), isPropagationStopped(), persist()), поэтому код, написанный для событий React, продолжает работать, хотя песочница передаёт объект события непосредственно вашему обработчику. Событие страницы пересекает границу, когда элемент, с которым взаимодействовал пользователь, или один из его предков в вашем компоненте имеет обработчик для этого типа события, либо когда это нажатие, которого ожидают ваши слушатели document (см. ниже). Оно передаётся один раз, а затем распространяется внутри песочницы так же, как на странице: event.target — элемент, с которым взаимодействовал пользователь, currentTarget — элемент, обработчик которого выполняется, stopPropagation() предотвращает выполнение обработчиков предков, а relatedTarget указывает на элемент песочницы, с которого или на который переместился фокус либо указатель, или имеет значение null, если этот элемент находится вне вашего компонента. Как и в React, onFocus и onBlur также выполняются при изменениях фокуса внутри элемента, а onClickCapture и другие обработчики перехвата выполняются на фазе перехвата. Обработчики, которые библиотека добавляет с помощью React.cloneElement, например используемые свойством render Base UI, выполняются после собственных обработчиков элемента, а не заменяют их, поэтому передача onClick: undefined в cloneElement не удаляет обработчик элемента. Обработчик на элементе render Base UI всё ещё может вызвать event.preventBaseUIHandler(), чтобы пропустить собственный обработчик Base UI. event.target.value и checked отражают состояние контрола после того, как браузер применил изменение пользователя: в обработчиках input, change, click, blur и focusout, а также в обработчиках keyup для value. В обработчиках keydown, beforeinput и указателя они содержат значение и состояние checked контрола на момент его последнего изменения, независимо от того, что отслеживает ваш компонент: обработчик Enter считывает введённый к этому моменту текст, но обработчик для печатной клавиши ещё не видит эту клавишу, поэтому считывайте актуальные значения из контролируемого input или обработчика onChange. Слушатели document получают события, пересекающие границу из вашего компонента, за одним исключением для нажатий. Когда ваш компонент прослушивает pointerdown, pointerup, pointercancel, mousedown, mouseup или click в document, как это делает открытое всплывающее окно, чтобы закрываться при нажатиях снаружи, все нажатия достигают этих слушателей: нажатие внутри вашего компонента отправляется элементу, который нажал пользователь, даже если ничто его не обрабатывает, а нажатие в другом месте страницы отправляется в document.body со своим состоянием кнопки, числом кликов и типом указателя, но без координат или клавиш-модификаторов. Клики без нажатия, например при активации клавиатурой элемента управления вне вашего компонента, и нажатия на полосах прокрутки страницы не отправляются, а касательное движение за пределами вашего компонента приходит только как pointerdown и pointercancel, поэтому оно не закрывает открытое всплывающее окно. Ничто другое из остальной части страницы не пересекает границу, поэтому, чтобы перехватывать сочетания клавиш с помощью document.addEventListener('keydown', ...), добавьте onKeyDown к элементу, который оборачивает ваш компонент. Обработчики window получают только error и unhandledrejection. Перемещения указателя передаются только над элементами, имеющими обработчик перемещения, а захват указателя остаётся на странице, поэтому перетаскивание прекращается, как только указатель покидает эти элементы. 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 страницы для разрешения относительного пути.

Выбор файла

Элементы выбора файлов предоставляют нативные объекты File с метаданными и доступным для чтения содержимым. Используйте text() или arrayBuffer() для чтения выбранного файла либо передайте его хост-функции uploadFile. Для открытия нативного средства выбора необходимо вызвать input.click() в течение одной секунды после доверенного щелчка внутри вашего компонента. Сценарии React и Preact, созданные с помощью SDK, проверены в Chromium. URL-адреса объектов, созданных Worker и назначенные img.src, поддерживают предварительный просмотр. Отзывайте URL-адреса при замене изображения или размонтировании компонента. Ваше приложение отвечает за проверку файлов, ход загрузки и отмену.

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

Объявите microphone, camera или оба параметра в массиве requestedCapabilities, передаваемом в defineApplication(), чтобы эти возможности отображались при установке. Одобрение действует для всей рабочей области и требует участника, который может управлять приложениями. Если установленное приложение вызывает navigator.mediaDevices.getUserMedia() без необходимого доступа, Twenty приостанавливает захват и отображает диалоговое окно одобрения. Это также поддерживает более старые приложения без объявлений возможностей. Участники, которые не могут управлять приложениями, должны попросить администратора рабочей области одобрить доступ. Отмена диалогового окна отклоняет запрос до запроса разрешений браузера или операционной системы. Обновления пакета сохраняют существующие разрешения рабочего пространства и не предоставляют автоматически дополнительный доступ к медиа. Локально синхронизированные приложения для разработки получают возможности, объявленные в их манифесте. После одобрения доступа Twenty не запрашивает его снова для каждого захвата, однако разрешения браузера и операционной системы по-прежнему действуют. Оранжевая точка в заголовке рабочего пространства указывает на активный захват. Наведите указатель на точку или выберите ее, чтобы увидеть приложения, использующие микрофон или камеру, и остановить их захват. Ожидающие запросы отображаются в виде контурной точки с элементом управления отменой. getUserMedia() и MediaRecorder работают через полифилы песочницы, поэтому стандартный код записи и MediaRecorder.isTypeSupported() работают без изменений после одобрения. Подробные объекты ограничений принимаются, но не перенаправляются. Хост выполняет захват с настройками по умолчанию для запрошенных типов. Разные приложения могут выполнять захват одновременно, а остановка приложения влияет только на его собственные сеансы. Каждый фронт-компонент поддерживает только один ожидающий или активный захват одновременно. Сохраните записанный Blob с помощью хост-функции uploadFile.

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

  • Перетащите приложения. Перетаскивайте события, но event.dataTransfer неопределённый.
  • Узел встроенный fs, path и node:crypto не срабатывают сборки, так что переместитесь с помощью logic функции. Web Crypto заменяет node:crypto, также доступны fetch, TextEncoder и URL, см. глобальную область видимости — но ничто не заменяет fs: в песочнице нет файловой системы.
  • iframe всегда повторно изолируется без allow-same-origin, поэтому встраиваемый контент, зависящий от собственной сессии, отображается как неавторизованный. У него также нет onLoad.