snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
---
title: Элементы меню команд
description: Отображайте фронтенд-компоненты как быстрые действия и элементы командного меню (Cmd+K) с помощью `defineCommandMenuItem`.
icon: terminal
---
**Элемент командного меню** — это мост между пользователем и [фронтенд-компонентом](/l/ru/developers/extend/apps/layout/front-components). Он регистрирует компонент в командном меню Twenty (Cmd+K) и, при необходимости, как закреплённую кнопку быстрого действия в правом верхнем углу страницы.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Поля конфигурации
| Поле | Обязательно | Описание |
| --------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Да | Стабильный уникальный идентификатор для команды |
| `label` | Да | Полная метка, отображаемая в меню команд (Cmd+K) |
| `frontComponentUniversalIdentifier` | Да | `universalIdentifier` фронтенд-компонента, который открывается этой командой |
| `shortLabel` | Нет | Короткая метка, отображаемая на закреплённой кнопке быстрого действия |
| `icon` | Нет | **Устарело** — игнорируется в пользу значка приложения; при установке этого параметра сборка выводит предупреждение |
| `isPinned` | Нет | При значении `true` показывает команду как кнопку быстрого действия в правом верхнем углу страницы |
| `availabilityType` | Нет | Определяет, где отображается команда: `'GLOBAL'` (доступна всегда), `'GLOBAL_OBJECT_CONTEXT'` (только на страницах с объектным контекстом — индексные страницы и карточки записей), `'RECORD_SELECTION'` (только при выборе записей) или `'FALLBACK'` (показывается, когда другие команды не подходят) |
| `availabilityObjectUniversalIdentifier` | Нет | Ограничивает команду страницами определённого типа объектов (например, только для записей Company) |
| `conditionalAvailabilityExpression` | Нет | Логическое выражение, которое динамически управляет видимостью (см. ниже) |
## Команды без интерфейса
Элемент командного меню в паре с [фронтенд-компонентом без интерфейса](/l/ru/developers/extend/apps/layout/front-components#headless-vs-non-headless) — идиоматичный способ предоставить действие в один клик: выполнить код, перейти или подтвердить и выполнить. Страница Front Components описывает [SDK Command components](/l/ru/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`), которые реализуют шаблон «действие и размонтирование».
Типичный сценарий: headless-компонент рендерит `<Command execute={...} />` (см. [полный пример](/l/ru/developers/extend/apps/layout/front-components#sdk-command-components)), и пункт меню команды указывает на него:
```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',
});
```
## Выражения условной доступности
Поле `conditionalAvailabilityExpression` позволяет управлять видимостью команды в зависимости от текущего контекста страницы. Импортируйте типизированные переменные и операторы из `twenty-sdk`, чтобы составлять выражения:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` уже подразумевает, что есть выбранные записи — используйте `numberOfSelectedRecords` только для конкретных количеств (например, `>= 2`).
</Note>
### Переменные контекста
Они представляют текущее состояние страницы:
| Переменная | Тип | Описание |
| ------------------------------ | --------- | ------------------------------------------------------------------------ |
| `pageType` | `string` | Текущий тип страницы (например, `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Указывает, рендерится ли компонент в боковой панели |
| `numberOfSelectedRecords` | `number` | Количество выбранных в данный момент записей |
| `isSelectAll` | `boolean` | Активен ли режим "выбрать все" |
| `selectedRecords` | `array` | Объекты выбранных записей |
| `favoriteRecordIds` | `array` | ID избранных записей |
| `objectPermissions` | `object` | Разрешения для текущего типа объекта |
| `targetObjectReadPermissions` | `object` | Права на чтение для целевого объекта |
| `targetObjectWritePermissions` | `object` | Права на запись для целевого объекта |
| `featureFlags` | `object` | Активные флаги функций |
| `objectMetadataItem` | `object` | Метаданные текущего типа объекта |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Есть ли у текущего представления фильтр мягкого удаления |
### Операторы
Комбинируйте переменные в логические выражения:
| Оператор | Описание |
| ----------------------------------- | ------------------------------------------------------------------------- |
| `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`, если свойство не равно значению ни у одного элемента |

View File

@@ -0,0 +1,849 @@
---
title: Компоненты фронтенда
description: Создавайте компоненты React, которые отображаются внутри интерфейса Twenty в изолированной песочнице.
icon: window-maximize
---
Фронтенд-компоненты — это компоненты React, которые отображаются непосредственно внутри интерфейса Twenty. Они выполняются в **изолированном Web Worker** с использованием Remote DOM — ваш код исполняется внутри изолированного iframe с непрозрачным источником (opaque-origin), но его UI всё равно нативно рендерится на странице, а не ограничивается этим iframe.
<Warning>
Конструкции передней части все еще активно развиваются. Ваш код запускается с частичным DOM, а не с реальной страницей браузера, так что использование расширенного кода может привести к ошибке, часто молчанию. См. [Текущие ограничения](#current-limitations).
</Warning>
## Где можно использовать фронт-компоненты
Фронт-компоненты могут отображаться в трёх местах внутри 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 (
<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,
});
```
```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`) быстрое действие появится в правом верхнем углу страницы:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Кнопка быстрого действия в правом верхнем углу" />
</div>
Нажмите её, чтобы отобразить компонент инлайн.
## Поля конфигурации
| Поле | Обязательно | Описание |
| --------------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| `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 (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
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 <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,
});
```
```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 (
<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,
});
```
И пример использования `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 (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
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://\<your-workspace-subdomain>.withtwenty.com\<path>`. Для внешних вызовов скопируйте точный 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 <Command execute={execute} />;
};
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,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` внедряется автоматически — см. [переменные приложения](#application-variables). Поскольку секретные переменные приложения никогда не раскрываются front-компонентам, храните ключи API и другую конфиденциальную логику в логической функции, а не во front-компоненте.
</Note>
### Вызов 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 (
<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` или `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 <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Секретные переменные (`isSecret: true`) **не** доступны фронтенд-компонентам. Они доступны только в [логических функциях](/l/ru/developers/extend/apps/logic/logic-functions), которые выполняются на стороне сервера. Это предотвращает отправку в браузер конфиденциальных значений, таких как ключи API.
</Warning>
`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://\<your-workspace-subdomain>.withtwenty.com` или основном общедоступном домене приложения, если он настроен), чтобы ответы, сформированные приложением, отдавались с изолированного источника, а не с источника приложения Twenty. Самостоятельно развёрнутые и локальные экземпляры обслуживают маршруты приложения с префиксом `/s` на самом сервере и могут вовсе не задавать эту переменную. Поскольку базовый URL различается для каждого рабочего пространства и экземпляра, ваш код не может жёстко прописать его — сервер внедряет правильное значение во время выполнения.
Вам редко нужно читать его напрямую. Вызывайте свои маршруты через `RestApiClient` с путём, начинающимся с `/s/`, и клиент разрешит URL за вас: он убирает префикс `/s` и обращается к `TWENTY_FUNCTIONS_URL`, а если переменная не задана, использует `\<TWENTY_API_URL>/s`. Используйте `resolveUrl('/s/\<path>')`, чтобы получить абсолютный 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 (
<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,
});
```
### Хранилище
`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 (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty хранит значения от имени вашего приложения, поэтому запись применяется локально сразу же и сохраняется в фоновом режиме. Операции чтения никогда не блокируются ожиданием хоста. Ничто не синхронизируется: значения не следуют за пользователем в другой браузер или на другой компьютер, поэтому используйте [хранилище ключ-значение](/l/ru/developers/extend/apps/logic/key-value-store) логической функции для всего, что должно пережить смену устройства.
Записи ограничены, и для каждого лимита считаются символы, а не байты: ключи могут содержать не более 512 символов, одно значение — не более 262144 символов, а каждое хранилище — не более 1048576 символов на приложение и пользователя. Запись, нарушающая лимит, выбрасывает `QuotaExceededError`, как и браузерный API.
### Работа с несколькими записями
Используйте `useSelectedRecordIds()` для обработки нескольких выбранных записей. Это полезно для массовых операций:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
Сделайте его доступным через [элемент командного меню](/l/ru/developers/extend/apps/layout/command-menu-items), доступный только при выборе записей:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Публичные ресурсы
Компоненты фронтенда могут получать доступ к файлам из каталога приложения `public/` с помощью `getPublicAssetUrl`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
См. [раздел о публичных ресурсах](/l/ru/developers/extend/apps/config/public-assets) для подробностей.
## Распространение зависимостей между фронт-компонентами
По умолчанию, каждая передняя часть объединяет свою собственную копию импортируемых библиотек, так что приложение с пятью компонентами поставляет React пять раз. Объявление общих зависимостей в пакете вашего приложения. son\`, чтобы собрать эти библиотеки один раз и загрузить каждый компонент приложения из одного кэшированного файла:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Затем каждый компонент импортирует его зависимости точно так же, как и раньше: ничего не изменяется в коде компонента:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Несколько важных моментов:
* **Один общий набор зависимостей для каждого приложения.** Пакет построен из зависимостей вашего приложения, так что вы сохраняете полный контроль над версиями, которые вы поставляете.
* **Перечислите точные спецификаторы, которые вы импортируете.** `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](#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`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). Компоненты фронтенда могут использовать его для кнопок, тегов, статусных плашек, чипов, аватаров, иконок, типографики и токенов темы, которые автоматически соответствуют светлой и тёмной теме рабочего пространства.
### Установка
Добавьте пакет в своё приложение, зафиксировав его на версии, с которой поставляется ваш экземпляр Twenty:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` включается в ваш компонент фронтенда на этапе сборки, поэтому его достаточно иметь в зависимостях вашего приложения — во время выполнения ничего настраивать не нужно.
### Импорт компонентов
Импортируйте из соответствующего подпути, а не из корня пакета, чтобы в ваш бандл попали только те компоненты, которые вы используете:
| Подпуть | Что экспортирует |
| --------------------------- | ------------------------------------------------- |
| `twenty-ui/input` | `Button` и элементы ввода формы |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` и другие |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` и другие |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` и другие |
| `twenty-ui/icon` | Компоненты `Icon*` (например, `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### Иконки
Импортируйте отдельные иконки из `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Каждая именованная иконка участвует в tree-shaking, поэтому импорт нескольких иконок почти не увеличит размер вашего бандла. Избегайте `IconsProvider`, `useIcons` и `iconsState` — они подключают весь набор иконок Tabler (несколько мегабайт).
### Темизация и токены темы
Компоненты Twenty UI автоматически подстраиваются под светлую и тёмную темы рабочего пространства — рендерер применяет активную цветовую схему на хосте, а компоненты вычисляют свои цвета относительно неё.
Чтобы использовать те же дизайн‑токены в собственных встроенных стилях, вызовите хук `useTheme()`. Он возвращает токены темы Twenty (отступы, цвета, радиусы, шрифты), привязанные к активной теме, без необходимости настраивать `ThemeProvider` в вашем компоненте:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Поскольку `useTheme()` — это хук, вы читаете токены внутри тела компонента, поэтому значения всегда соответствуют активной теме. Та же карта токенов также экспортируется как константа `themeCssVariables`, но в компонентах фронтенда предпочтительнее использовать `useTheme()` — модульная константа, разыменующая `themeCssVariables`, может быть `undefined`, пока извлекается манифест приложения.
Чтобы явно разветвлять логику по активной цветовой схеме, считайте её с помощью `useColorScheme()` из `twenty-sdk/front-component`, который возвращает `'light'` или `'dark'`.
## Нынешние ограничения
Активно разрабатываются передние компоненты. Рендеринг, стилизация, обработка событий, измерение элементов и работа с хранилищем браузера работают хорошо. Все, что выходит *за рамки* этих возможностей (вызов DOM‑метода на ref, отслеживание изменения размеров элементов, порталирование за пределы вашего дерева), сегодня отсутствует или реализовано не полностью, и в большинстве случаев это тихо ломается: ни исключения, ни ошибки TypeScript, поскольку обвязка типизирована под полный DOM браузера.
Если один из этих блоков вас, [откройте задачу](https://github.com/twentyhq/twenty/issues/new/choose), чтобы получить приоритет.
### Макет и измерение
Элементы могут измерять себя сами: хост отражает геометрию в песочницу, поэтому чтения выполняются локально, но могут отставать до одного кадра, а первое чтение никогда ранее не измеренного элемента возвращает нули. После записи выполните повторное чтение в колбэке `requestAnimationFrame` или в эффекте.
| API | Что происходит |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Работают, из зеркала; присваивание `scrollTop` / `scrollLeft` не имеет эффекта |
| `getClientRects()`, `window.matchMedia()` | Бросает |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Работают, сообщая размеры области просмотра браузера; собственный размер вашего виджета — `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Возвращает только встроенные (inline) стили элемента и никогда не возвращает вычисленный хостом каскад |
| `ResizeObserver`, `IntersectionObserver` | `Справочная ошибка` (защита `typeof` делает работу) |
| `MutationObserver` | Работает, включая `subtree`, `attributeFilter`, старые значения и `takeRecords()` |
Позиционирование с использованием `getBoundingClientRect` теперь работает, но всё, что отслеживает изменение размеров через `ResizeObserver` (recharts `ResponsiveContainer`, `autoUpdate` из Floating UI), по‑прежнему не работает. В любом случае отдавайте предпочтение CSS для верстки: ваша таблица стилей применяется к реальной странице, поэтому flexbox, grid, `aspect-ratio`, `clamp()` и `@container` работают как обычно, без задержки кадра.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` и `queueMicrotask` работают без префикса `window.`. Только `window.requestAnimationFrame(...)` и брошенные друзья.
</Note>
### DOM access
`ref` дает вам элемент песочницы, а не `HTMLElement`.
| Что вы пишете | Что происходит | Использовать вместо |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Бросает | Контролируемые компоненты; читать значения из `event.target` |
| `element.classList.add(...)` | Броски (`classList` `undefined`) | Создайте строку `className` самостоятельно |
| `document.createTreeWalker()` | Бросает | `querySelector()` / `querySelectorAll()` и `getElementById()` работают; `getElementsByClassName()` тоже работает, но возвращает статическую коллекцию, а не «живой» `HTMLCollection` |
| `document.activeElement` | Всегда `неопределённые` | Отслеживать фокус с с помощью «onFocus» / «onBlur» |
| `canvas` | Отображает ничего, без ошибок | SVG или рисуйте вне экрана и показывайте результат в элементе `img` |
| `createPortal(node, document.body)` | Отображает ничего, в то время как «isConnected» сообщает об успехе | Наслоения в строке `position: absolute` или передайте библиотеке свой собственный элемент контейнера |
Портал зазор поэтому всплывающие окна 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](#storage) для получения информации об области действия и ограничениях. IndexedDB, cookies, Cache API и `BroadcastChannel` по-прежнему недоступны. Чтобы сохранять состояние между устройствами, вызовите [логическую функцию](/l/ru/developers/extend/apps/logic/logic-functions) и используйте ее [хранилище ключ-значение](/l/ru/developers/extend/apps/logic/key-value-store).
`fetch` работает с сохранениями:
* Звонки к Twenty API и маршрутам вашего приложения проксируются узлом, поэтому предпочитайте [`RestApiClient`](#calling-the-twenty-rest-api). В случае проксируемых вызовов `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 функции](/l/ru/developers/extend/apps/logic/logic-functions). Веб-криптовалюты, `fetch`, `TextEncoder` и `URL`.
* **`iframe`** всегда повторно изолируется без `allow-same-origin`, поэтому встраиваемый контент, зависящий от собственной сессии, отображается как неавторизованный. У него также нет `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Элементы навигационного меню
description: Добавляйте на боковую панель рабочего пространства пользовательские элементы — ссылки на сохраненные представления или внешние URL-адреса.
icon: bars
---
**Элемент навигационного меню** — это запись в левой боковой панели. Используйте `defineNavigationMenuItem()` для добавления пользовательских ссылок на боковую панель — обычно по одной на каждое [представление](/l/ru/developers/extend/apps/layout/views), которое вы поставляете, — или для указания на внешние URL-адреса.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## Основные моменты
* `type` определяет, на что указывает элемент меню. Каждый тип сопоставляется с определенным полем идентификатора:
| Тип | Что делает | Обязательное поле |
| ------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Открывает сохраненное представление | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Открывает внешний URL-адрес | `link` |
| `NavigationMenuItemType.FOLDER` | Группирует вложенные элементы под меткой | `name` (а дочерние элементы ссылаются на папку через `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Открывает страницу индекса по умолчанию объекта | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Открывает отдельный макет страницы | `pageLayoutUniversalIdentifier` |
* `position` управляет порядком в боковой панели.
* Перечисление также содержит `NavigationMenuItemType.RECORD`, используемый внутренне для добавляемых пользователем в «Избранное» записей — он не может использоваться из манифеста приложения (нет поля для ссылки на запись).
* `icon` и `color` являются необязательными и настраивают внешний вид элемента.
* `folderUniversalIdentifier` также доступен для любого элемента, чтобы поместить его внутрь родительского элемента типа `FOLDER`.
<Note>
**Распространенная ошибка:** создание объекта без связанного элемента навигационного меню делает этот объект недоступным через боковую панель. Если только это не технический/внутренний объект, у каждого настраиваемого объекта должен быть элемент боковой панели, указывающий на него.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Обзор
description: Разместите своё приложение внутри интерфейса Twenty — элементы боковой панели, сохранённые представления, вкладки страницы записи и изолированные React‑компоненты.
icon: table-columns
---
**Слой компоновки** приложения Twenty — это всё, что видит пользователь: где приложение отображается в боковой панели, какие списки представлений оно поставляет, как устроены его страницы деталей записей и какие пользовательские React‑компоненты отображаются внутри этих страниц.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## В этом разделе
<CardGroup cols={2}>
<Card title="Представления" icon="list" href="/l/ru/developers/extend/apps/layout/views">
`defineView` — сохранённые конфигурации списков: видимые столбцы, фильтры, группы.
</Card>
<Card title="Элементы меню навигации" icon="bars" href="/l/ru/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — элементы боковой панели, ссылающиеся на представления или внешние URL.
</Card>
<Card title="Макеты страниц" icon="table-columns" href="/l/ru/developers/extend/apps/layout/page-layouts">
`definePageLayout` и `definePageLayoutTab` — вкладки и виджеты на странице деталей записи.
</Card>
<Card title="Компоненты фронтенда" icon="window-maximize" href="/l/ru/developers/extend/apps/layout/front-components">
`defineFrontComponent` — изолированные React‑компоненты, которые отображаются внутри Twenty.
</Card>
<Card title="Элементы меню команд" icon="terminal" href="/l/ru/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — регистрирует фронтенд‑компоненты как элементы Cmd+K и быстрые действия.
</Card>
</CardGroup>
## Где отображается приложение
| Область отображения | Что определяет | Сущность |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Боковая панель** | Пользовательский элемент, который ссылается на сохранённое представление или внешний URL | `defineNavigationMenuItem` |
| **Список записей** | Сохранённая конфигурация для объекта — видимые столбцы, порядок, фильтры, группы | `defineView` |
| **Страница деталей записи** | Вкладки и виджеты на странице записи (вашего собственного объекта или стандартного) | `definePageLayout`, `definePageLayoutTab` |
| **Внутри любого из перечисленного выше** | Пользовательский React‑виджет — кнопки, формы, панели мониторинга, интеграции | `defineFrontComponent` |
| **Настройки приложения** | Настраиваемый раздел конфигурации на вкладке «Настройки» приложения, используемый вместо стандартного интерфейса переменных | `defineSettingsFrontComponent` |
| **Меню команд (Cmd+K)** | Закреплённое быстрое действие или скрытая команда | `defineCommandMenuItem` |
Фронтенд‑компоненты выполняются внутри изолированного Web Worker с использованием Remote DOM — они отображаются *нативно* на странице (не внутри iframe), но не могут напрямую обращаться к хостовой странице или DOM. Взаимодействие с Twenty происходит через хостовый API обмена сообщениями.
Они все еще находятся в активной разработке: песочница реализует частичный DOM, поэтому сложные варианты использования могут завершаться сбоем. См. раздел [Текущие ограничения](/l/ru/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Макеты страниц
description: Настраивайте страницы деталей записей — вкладки, виджеты и места, где отображаются фронтенд-компоненты, — с помощью `definePageLayout` и `definePageLayoutTab`.
icon: table-columns
---
**Макет страницы** управляет тем, как устроена страница деталей записи: какие вкладки отображаются и какие виджеты они содержат. Используйте `definePageLayout()` для объявления макета для объекта, которым вы владеете, или `definePageLayoutTab()` для добавления одной вкладки к макету, который уже существует (вашему или стандартному Twenty).
| Сценарий использования | Сущность |
| --------------------------------------------------------------------------------------------- | --------------------- |
| Определите весь макет для страницы записи на объекте, которым вы владеете | `definePageLayout` |
| Добавьте одну вкладку в существующий макет (для вашего собственного объекта или стандартного) | `definePageLayoutTab` |
## definePageLayout
Используйте это, когда вы управляете всей страницей деталей — обычно для пользовательского объекта, который вы определили сами.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### Основные моменты
* `type` — одно из значений `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` или `'STANDALONE_PAGE'`. Используйте `'RECORD_PAGE'` для настройки детального представления конкретного объекта.
* `objectUniversalIdentifier` указывает, к какому объекту применяется этот макет.
* Каждая `tab` определяет раздел страницы с `title`, `position` и `layoutMode`: `VERTICAL_LIST` для страниц записей и автономных страниц, `GRID` для панелей мониторинга и `CANVAS` для одного виджета, который должен заполнять область просмотра вкладки. Во вкладке `VERTICAL_LIST` виджеты располагаются вертикально. Встроенные виджеты, которые самостоятельно управляют прокруткой, такие как временные шкалы, файлы, заметки, задачи и рабочие процессы, заполняют одну область просмотра; поля, фронтальные компоненты, графики и другие виджеты с подгонкой по содержимому отображаются с высотой по содержимому или заданной высотой. Во вкладке `GRID` виджеты всегда размещаются в виде карточек в 12-колоночной сетке. Виджет `CANVAS` не имеет явно заданного положения; если вкладка холста содержит несколько виджетов, они отображаются на высоте своего содержимого, а не заполняют область просмотра.
* Явно задайте `layoutMode`. Если его не указывать, вы получите `VERTICAL_LIST` на `STANDALONE_PAGE` и `GRID` в остальных случаях, что редко бывает нужно на странице записи.
* Каждый `widget` внутри вкладки может отображать [front component](/l/ru/developers/extend/apps/layout/front-components), список связей или другие встроенные типы виджетов.
* Виджет `FRONT_COMPONENT` может задавать для `headerCommandMenuItemUniversalIdentifiers` упорядоченный массив универсальных идентификаторов элементов меню команд из того же приложения. Эти действия отображаются как кнопки со значками в заголовке карточки виджета и сохраняют доступность и проверки разрешений на уровне команд. Идентификаторы должны быть уникальными и разрешаться при установке приложения.
* `position` у вкладок управляет их порядком. Используйте большие значения (например, 50), чтобы разместить пользовательские вкладки после встроенных.
### Виджеты полей
Виджет `FIELD` отображает одно поле записи. Для полей связи он также может встраивать список связанных записей:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` принимает универсальный идентификатор поля в объекте макета.
* `fieldDisplayMode` может иметь одно из следующих значений: `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` или `'TABLE'`. `TABLE` встраивает представление, отображающее записи поля связи «один‑ко‑многим».
* `nestedRelationFieldMetadataId` является необязательным и принимает универсальный идентификатор поля связи «один‑ко‑многим» на целевом объекте связи, чтобы перечислять записи на два перехода связи дальше (например, страница Company, перечисляющая сделки людей этой компании, или страница Person, перечисляющая сделки компании этого человека). Первый переход может быть полем связи «один‑ко‑многим» или «многие‑к‑одному», второй обязан быть «один‑ко‑многим» (соединительные связи не поддерживаются), и это требует `fieldDisplayMode: 'TABLE'` — комбинирование его с любым другим режимом отображения является ошибкой валидации, поскольку вложенный виджет всегда отображается как встраиваемое представление.
## definePageLayoutTab
Используйте это, когда вы хотите только **добавить** вкладку к существующему макету — например, вкладку аналитики на стандартной странице Company или вкладку с AI-сводкой, прикреплённую к макету вашего собственного объекта.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Основные моменты
* `pageLayoutUniversalIdentifier` является **обязательным** и должен указывать на макет страницы, который уже существует на момент установки — либо стандартный макет Twenty, либо определённый вашим собственным приложением. Кросс-приложенческие ссылки на макеты, которыми владеет другое установленное приложение, на данный момент не поддерживаются. Если родительский макет отсутствует, установка завершается с понятной ошибкой проверки.
* Для стандартных макетов Twenty импортируйте идентификаторы из `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Каждый элемент макета также предоставляет свои `tabs` и их `widgets`, поэтому вы можете ссылаться на любой уровень:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Также доступен короткий псевдоним `STANDARD_PAGE_LAYOUT`:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` ограничены только этой вкладкой — они ссылаются на [front components](/l/ru/developers/extend/apps/layout/front-components), представления и т. п. точно так же, как виджеты, определённые непосредственно в `definePageLayout`.
* `position` управляет порядком относительно существующих вкладок в целевом макете. Выберите значение, которое поместит вашу вкладку в нужное место относительно встроенных вкладок.
* Используйте это вместо `definePageLayout`, когда вы хотите только добавить к существующему макету. Используйте `definePageLayout`, когда вы управляете всем макетом.

View File

@@ -0,0 +1,113 @@
---
title: Представления
description: Поставляйте предварительно настроенные сохранённые представления — порядок столбцов, фильтры, группы — для объектов в вашем приложении.
icon: list
---
**Представление** — это сохранённая конфигурация того, как отображаются записи объекта: какие поля появляются, их порядок, видимость, а также применённые фильтры и группы. Используйте `defineView()` для поставки преднастроенных представлений вместе с вашим приложением.
Каждый объект автоматически получает свое основное представление списка (таблицу "All ...", которую открывает пункт навигации `OBJECT`), подготавливаемое сервером — включая объекты, создаваемые вашим приложением, с одним столбцом для каждого поля, объявленного вашим приложением. Представления, которые вы определяете с помощью `defineView()`, всегда являются **дополнительными** представлениями поверх этого представления по умолчанию.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## Основные моменты
* `objectUniversalIdentifier` указывает, к какому объекту применяется это представление. Это может быть пользовательский объект, который вы определили, или стандартный объект Twenty.
* Основное представление списка объекта принадлежит серверу: `key` устарел и игнорируется, поэтому представление из манифеста никогда не сможет на него претендовать. Добавьте элемент навигации `VIEW`, если вы хотите, чтобы до вашего представления можно было добраться в один клик из боковой панели.
* `fields` управляет тем, какие столбцы отображаются и в каком порядке. Каждое поле ссылается на `fieldMetadataUniversalIdentifier`. Чтобы сослаться на автоматически созданное системное поле, такое как `createdAt`, см. раздел [Targeting System Metadata](/l/ru/developers/extend/apps/data/system-fields).
* Также вы можете определить `filters`, `filterGroups`, `sorts`, `groups` и `fieldGroups` для более продвинутых конфигураций.
* `position` управляет порядком, когда для одного и того же объекта существует несколько представлений.
## Необязательные свойства
| Свойство | Значения | Описание |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (по умолчанию), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Как записи располагаются на экране. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` и `CALENDAR_WIDGET` также существуют, но используются внутри виджетов макета страницы.) |
| `visibility` | `ViewVisibility.WORKSPACE` (по умолчанию), `ViewVisibility.UNLISTED` | Отображается ли представление для всего рабочего пространства или скрыто из элементов выбора. |
| `openRecordIn` | устаревшие | Больше не используется: место открытия записей теперь является свойством [объекта](/l/ru/developers/extend/apps/data/objects) (`openRecordIn` в `defineObject()`); при отсутствии значения используется предпочтение каждого пользователя. |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Порядок сортировки по умолчанию. |
| `isCompact` | `boolean` | Компактное отображение строк. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Группировать записи (например, столбцы канбана) по полю. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Агрегирование и ширина столбцов канбана. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Календарные представления: макет, поле даты, по которому размещаются записи, и необязательное поле даты окончания. |
Все перечисления выше экспортируются из `twenty-sdk/define`.
## Фильтры
Представление может поставляться с заранее примененными фильтрами. У каждого фильтра есть три координаты: **поле**, по которому выполняется фильтрация, **операнд** (как сравнивать) и **значение** (с чем сравнивать). Все три должны совпадать — использование операнда, который не подходит для типа поля, будет отклонено во время синхронизации.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Поддерживаемые операнды по типу поля
| Тип поля | Поддерживаемые операнды |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Типы полей с похожими названиями могут использовать совершенно разные операнды — типичный пример: `SELECT` и `MULTI_SELECT`.
### Форма значения для каждого операнда
Поле `value` всегда содержит значение, сериализуемое в JSON, но ожидаемая форма зависит от операнда:
| Семейство операндов | Форма значения | Пример |
| ------------------------------------------------------ | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` для `SELECT` | массив ключей опций (строки) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` для `MULTI_SELECT` | массив ключей опций (строки) | `['TAG_A']` |
| `IS`, `IS_NOT` для `RELATION` | массив идентификаторов записей (uuid) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` для текстовых полей | строка | `'acme'` |
| `IS`, `IS_NOT` для `NUMBER` | строка (значение) | `'5'` |
| `IS` для `RATING` / `UUID` | строка (значение) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | строка (граница) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` для `DATE` / `DATE_TIME` | строка в формате ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | пустая строка | `''` |
| `IS` для `BOOLEAN` | `'true'` или `'false'` | `'true'` |
## Как представления отображаются в интерфейсе
Само по себе представление недоступно из боковой панели. Чтобы оно появилось там, свяжите его с [пунктом навигационного меню](/l/ru/developers/extend/apps/layout/navigation-menu-items) типа `VIEW`, который указывает на `universalIdentifier` представления. Это канонический шаблон: для каждого настраиваемого объекта при создании автоматически генерируется индексное представление по умолчанию, поэтому вам нужно только добавить элемент боковой панели, который его открывает.