Перейти к основному содержанию
Фронтенд-компоненты — это компоненты React, которые отображаются непосредственно внутри интерфейса Twenty. Они выполняются в изолированном Web Worker с использованием Remote DOM — ваш код изолирован (sandboxed), но рендерится нативно на странице, а не в iframe.

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

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

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

Самый быстрый способ увидеть фронтенд-компонент в действии — зарегистрировать его как команду. Добавление поля command с isPinned: true делает его кнопкой быстрого действия в правом верхнем углу страницы — макет страницы не требуется:
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';

const HelloWorld = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Hello from my app!</h1>
      <p>This component renders inside Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
  command: {
    universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
    shortLabel: 'Hello',
    label: 'Hello World',
    icon: 'IconBolt',
    isPinned: true,
    availabilityType: 'GLOBAL',
  },
});
После синхронизации с помощью yarn twenty dev (или однократного запуска yarn twenty dev --once) быстрое действие появится в правом верхнем углу страницы:
Кнопка быстрого действия в правом верхнем углу
Нажмите её, чтобы отобразить компонент инлайн.

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

ПолеОбязательноОписание
universalIdentifierДаСтабильный уникальный идентификатор для этого компонента
componentДаФункция компонента React
nameНетОтображаемое имя
descriptionНетОписание того, что делает компонент
isHeadlessНетУстановите значение true, если у компонента нет видимого пользовательского интерфейса (см. ниже)
commandНетЗарегистрируйте компонент как команду (см. параметры команды ниже)

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

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

Headless и non-headless

Фронт-компоненты поддерживают два режима отображения, управляемых опцией isHeadless: Non-headless (по умолчанию) — компонент отображает видимый интерфейс. При запуске из меню команд он открывается в боковой панели. Это поведение по умолчанию, когда isHeadless имеет значение false или опущен. Headless (isHeadless: true) — компонент монтируется невидимо в фоновом режиме. Он не открывает боковую панель. Компоненты headless предназначены для действий, которые выполняют логику и затем размонтируются — например, запуск асинхронной задачи, переход на страницу или показ модального окна подтверждения. Они естественно сочетаются с компонентами SDK Command, описанными ниже.
src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  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/command:
  • Command — запускает асинхронный колбэк через проп execute.
  • CommandLink — переходит по пути внутри приложения. Пропы: to, params, queryParams, options.
  • CommandModal — открывает модальное окно подтверждения. Если пользователь подтвердит, выполняет колбэк execute. Пропы: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — открывает конкретную страницу боковой панели. Пропы: page, pageTitle, pageIcon.
Полный пример headless фронт-компонента, использующего Command для запуска действия из меню команд:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
  command: {
    universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
    label: 'Run my action',
    icon: 'IconPlayerPlay',
  },
});
А также пример с использованием CommandModal для запроса подтверждения перед выполнением:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
  command: {
    universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
    label: 'Delete draft',
    icon: 'IconTrash',
  },
});

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

Внутри вашего компонента используйте хуки SDK для доступа к текущему пользователю, записи и экземпляру компонента:
src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk/front-component';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
Доступные хуки:
ХукВозвращаетОписание
useUserId()string или nullID текущего пользователя
useRecordId()string или nullID текущей записи (при размещении на странице записи)
useFrontComponentId()stringID этого экземпляра компонента
useFrontComponentExecutionContext(selector)различаетсяДоступ к полному контексту выполнения с помощью функции-селектора

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

Компоненты фронтенда могут вызывать навигацию, модальные окна и уведомления с помощью функций из twenty-sdk:
ФункцияОписание
navigate(to, params?, queryParams?, options?)Перейти на страницу в приложении
openSidePanelPage(params)Открыть боковую панель
closeSidePanel()Закрыть боковую панель
openCommandConfirmationModal(params)Показать диалог подтверждения
enqueueSnackbar(params)Показать всплывающее уведомление
unmountFrontComponent()Размонтировать компонент
updateProgress(progress)Обновить индикатор прогресса
Пример, который использует API хоста для показа snackbar и закрытия боковой панели после завершения действия:
src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  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 (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

Параметры команды

Добавление поля command в defineFrontComponent регистрирует компонент в меню команд (Cmd+K). Если isPinned имеет значение true, команда также отображается как кнопка быстрого действия в правом верхнем углу страницы.
ПолеОбязательноОписание
universalIdentifierДаСтабильный уникальный идентификатор для команды
labelДаПолная метка, отображаемая в меню команд (Cmd+K)
shortLabelНетКороткая метка, отображаемая на закреплённой кнопке быстрого действия
iconНетИмя значка, отображаемое рядом с меткой (например, 'IconBolt', 'IconSend')
isPinnedНетПри значении true показывает команду как кнопку быстрого действия в правом верхнем углу страницы
availabilityTypeНетОпределяет, где отображается команда: 'GLOBAL' (доступна всегда), 'RECORD_SELECTION' (только при выборе записей) или 'FALLBACK' (показывается, когда другие команды не подходят)
availabilityObjectUniversalIdentifierНетОграничивает команду страницами определённого типа объектов (например, только для записей Company)
conditionalAvailabilityExpressionНетЛогическое выражение для динамического управления видимостью команды (см. ниже)

Выражения условной доступности

Поле conditionalAvailabilityExpression позволяет управлять видимостью команды в зависимости от текущего контекста страницы. Импортируйте типизированные переменные и операторы из twenty-sdk, чтобы составлять выражения:
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  pageType,
  numberOfSelectedRecords,
  objectPermissions,
  everyEquals,
  isDefined,
} from 'twenty-sdk/front-component';

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'bulk-action',
  component: BulkAction,
  command: {
    universalIdentifier: '...',
    label: 'Bulk Update',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: everyEquals(
      objectPermissions,
      'canUpdateObjectRecords',
      true,
    ),
  },
});
Переменные контекста — представляют текущее состояние страницы:
ПеременнаяТипОписание
pageTypestringТекущий тип страницы (например, 'RecordIndexPage', 'RecordShowPage')
isInSidePanelbooleanУказывает, рендерится ли компонент в боковой панели
numberOfSelectedRecordsnumberКоличество выбранных в данный момент записей
isSelectAllbooleanАктивен ли режим “выбрать все”
selectedRecordsмассивОбъекты выбранных записей
favoriteRecordIdsмассивID избранных записей
objectPermissionsobjectРазрешения для текущего типа объекта
targetObjectReadPermissionsobjectПрава на чтение для целевого объекта
targetObjectWritePermissionsobjectПрава на запись для целевого объекта
featureFlagsobjectАктивные флаги функций
objectMetadataItemobjectМетаданные текущего типа объекта
hasAnySoftDeleteFilterOnViewbooleanЕсть ли у текущего представления фильтр мягкого удаления
Операторы — комбинируют переменные в логические выражения:
ОператорОписание
isDefined(value)true, если значение не null/undefined
isNonEmptyString(value)true, если значение — непустая строка
includes(array, value)true, если массив содержит значение
includesEvery(array, prop, value)true, если свойство каждого элемента включает значение
every(array, prop)true, если свойство истинно для каждого элемента
everyDefined(array, prop)true, если свойство определено у каждого элемента
everyEquals(array, prop, value)true, если свойство равно значению у каждого элемента
some(array, prop)true, если свойство истинно хотя бы у одного элемента
someDefined(array, prop)true, если свойство определено хотя бы у одного элемента
someEquals(array, prop, value)true, если свойство равно значению хотя бы у одного элемента
someNonEmptyString(array, prop)true, если свойство является непустой строкой хотя бы у одного элемента
none(array, prop)true, если свойство ложно для каждого элемента
noneDefined(array, prop)true, если свойство не определено ни у одного элемента
noneEquals(array, prop, value)true, если свойство не равно значению ни у одного элемента

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

Компоненты фронтенда могут получать доступ к файлам из каталога приложения public/ с помощью getPublicAssetUrl:
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';

const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
См. раздел о публичных ресурсах для подробностей.

Стилизация

Компоненты фронтенда поддерживают несколько подходов к стилизации. Вы можете использовать:
  • Встроенные стилиstyle={{ color: 'red' }}
  • Компоненты Twenty UI — импорт из twenty-sdk/ui (Button, Tag, Status, Chip, Avatar и другие)
  • Emotion — CSS-in-JS с @emotion/react
  • Styled-components — паттерны styled.div
  • Tailwind CSS — утилитарные классы
  • Любая библиотека CSS-in-JS, совместимая с React
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});