--- title: Frontendové komponenty description: Vytvářejte komponenty Reactu, které se vykreslují uvnitř uživatelského rozhraní Twenty se sandboxovou izolací. icon: window-maximize --- Frontendové komponenty jsou React komponenty, které se vykreslují přímo v uživatelském rozhraní Twenty. Běží v **izolovaném Web Workeru** s využitím Remote DOM — váš kód je sandboxovaný, ale vykresluje se nativně na stránce, nikoli v iframu. ## Kde lze použít frontendové komponenty Frontendové komponenty se mohou vykreslovat na dvou místech v rámci Twenty: * **Postranní panel** — Frontendové komponenty, které nejsou headless, se otevírají v pravém postranním panelu. Toto je výchozí chování, když je frontendová komponenta vyvolána z příkazového menu. * **Widgety (nástěnky a stránky záznamů)** — front komponenty lze vkládat jako widgety do [rozložení stránky](/l/cs/developers/extend/apps/layout/page-layouts). Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget frontendové komponenty. Samotná frontendová komponenta není z uživatelského rozhraní dostupná — je potřeba ji zpřístupnit. Dva způsoby, jak to udělat, jsou: * **Spárujte ji s [položkou příkazové nabídky](/l/cs/developers/extend/apps/layout/command-menu-items)** — zaregistruje ji v příkazové nabídce (Cmd+K) a volitelně také jako připnutou rychlou akci. * **Vložte ji jako widget do [rozložení stránky](/l/cs/developers/extend/apps/layout/page-layouts)** — umístí ji na detailní stránku záznamu nebo na nástěnku. ## Základní příklad Nejrychlejší způsob, jak vidět front komponentu v akci, je spárovat ji s [`defineCommandMenuItem`](/l/cs/developers/extend/apps/layout/command-menu-items), aby se objevila jako tlačítko rychlé akce v pravém horním rohu stránky: ```tsx src/front-components/hello-world.tsx import { defineFrontComponent } from 'twenty-sdk/define'; const HelloWorld = () => { return (

Hello from my app!

This component renders inside Twenty.

); }; export default defineFrontComponent({ universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', name: 'hello-world', description: 'A simple front component', component: HelloWorld, }); ``` ```ts src/command-menu-items/hello-world.command-menu-item.ts import { defineCommandMenuItem } from 'twenty-sdk/define'; export default defineCommandMenuItem({ universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', shortLabel: 'Hello', label: 'Hello World', icon: 'IconBolt', isPinned: true, availabilityType: 'GLOBAL', frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', }); ``` Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
Tlačítko rychlé akce v pravém horním rohu
Kliknutím na něj vykreslíte komponentu přímo ve stránce. ## Konfigurační pole | Pole | Povinné | Popis | | --------------------- | ------- | ----------------------------------------------------------------- | | `universalIdentifier` | Ano | Stabilní jedinečné ID pro tuto komponentu | | `component` | Ano | Funkce komponenty React | | `name` | Ne | Zobrazovaný název | | `description` | Ne | Popis toho, co komponenta dělá | | `isHeadless` | Ne | Nastavte na `true`, pokud komponenta nemá viditelné UI (viz níže) | ## Umístění frontendové komponenty na stránku Mimo příkazy můžete frontendovou komponentu vložit přímo na stránku záznamu přidáním jako widget v **rozvržení stránky**. Podrobnosti viz [Rozložení stránek](/l/cs/developers/extend/apps/layout/page-layouts). ## Headless vs. ne-headless Front-endové komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`: **Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena. **Headless (`isHeadless: true`)** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže. ```tsx 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, }); ``` Protože komponenta vrací `null`, Twenty přeskočí vykreslení kontejneru — v rozvržení se neobjeví žádné prázdné místo. Komponenta má však stále přístup ke všem hookům a API komunikace s hostitelem. ## Komponenty SDK Command Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front-endové komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front-endovou komponentu. Importujte je z `twenty-sdk/command`: * **`Command`** — Spustí asynchronní callback přes prop `execute`. * **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`. * **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. * **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`. Zde je kompletní příklad headless front-endové komponenty, která pomocí `Command` spouští akci z menu příkazů: ```tsx 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 ; }; 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', icon: 'IconPlayerPlay', frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', }); ``` A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením: ```tsx 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 ( ); }; export default defineFrontComponent({ universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', name: 'delete-draft', description: 'Deletes a draft with confirmation', component: DeleteDraft, isHeadless: true, }); ``` ## Volání logické funkce Front komponenty běží v prohlížeči v izolovaném web workeru, zatímco [logické funkce](/l/cs/developers/extend/apps/logic/logic-functions) běží na serveru. Neexistuje mezi nimi žádné přímé volání v rámci jednoho procesu — místo toho se front komponenta k logické funkci připojuje přes HTTP. Logická funkce deklarovaná pomocí `httpRouteTriggerSettings` je vystavena pod endpointem `/s/` na `${TWENTY_API_URL}/s\`. Vaše front komponenta volá tuto trasu pomocí `RestApiClient` z `twenty-client-sdk/rest`, který se autentizuje pomocí `TWENTY_APP_ACCESS_TOKEN`, který Twenty do workeru vkládá. `RestApiClient` je přesně pro tento účel. Z worker prostředí čte `TWENTY_API_URL` a `TWENTY_APP_ACCESS_TOKEN`, přidává hlavičku `Authorization: Bearer`, serializuje a parsuje JSON a vyhazuje `RestApiClientError`, pokud token nebo URL chybí nebo je odpověď mimo rozsah 2xx — takže nemusíte tento boilerplate znovu implementovat v každé komponentě. Headless front komponenta může volání spustit při mountu přes komponentu `Command` a poté se automaticky odmountovat: ```tsx src/front-components/sync-prs.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { Command } from 'twenty-sdk/command'; import { RestApiClient } from 'twenty-client-sdk/rest'; const SyncPrs = () => { const execute = async () => { const client = new RestApiClient(); await client.post('/s/github/fetch-prs', { owner: 'twentyhq', repo: 'twenty', }); }; return ; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'sync-prs', description: 'Triggers the fetch-prs logic function', isHeadless: true, component: SyncPrs, }); ``` Cesta předaná klientovi je veřejná cesta trasy — `httpRouteTriggerSettings.path` logické funkce s předponou `/s`. Ponechte `isAuthRequired: true`; klient poskytuje pro vaši komponentu přístupový token aplikace vydaný Twenty: ```ts src/logic-functions/fetch-prs.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; import type { RoutePayload } from 'twenty-sdk/logic-function'; const handler = async (event: RoutePayload) => { const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string }; // ...fetch from GitHub and persist records... return { ok: true }; }; export default defineLogicFunction({ universalIdentifier: '...', name: 'fetch-prs', handler, httpRouteTriggerSettings: { path: '/github/fetch-prs', httpMethod: 'POST', isAuthRequired: true, }, }); ``` `TWENTY_API_URL` a `TWENTY_APP_ACCESS_TOKEN` jsou vloženy automaticky — viz [Proměnné aplikace](#application-variables). Protože tajné proměnné aplikace nejsou nikdy vystaveny front komponentám, ponechte API klíče a další citlivou logiku v logické funkci, ne ve front komponentě. ### Reference `RestApiClient` Importujte `RestApiClient` z `twenty-client-sdk/rest`. Patří do stejné rodiny klientů jako `CoreApiClient` a `MetadataApiClient`, ale cílí na HTTP trasy vaší aplikace místo na GraphQL API. | Metoda | Popis | | --------------------------------- | ------------------------------------------ | | `get(path, options?)` | Odešle požadavek `GET` | | `post(path, body?, options?)` | Odešle požadavek `POST` | | `put(path, body?, options?)` | Odešle požadavek `PUT` | | `patch(path, body?, options?)` | Odešle požadavek `PATCH` | | `delete(path, options?)` | Odešle požadavek `DELETE` | | `request(method, path, options?)` | Obecný požadavek s libovolnou metodou HTTP | `options` přijímá `headers`, `query` (záznam parametrů dotazovacího řetězce; hodnoty typu nullish jsou vynechány) a `AbortSignal` prostřednictvím `signal`. Objekt `body`, který není typu `FormData`, je automaticky serializován do JSON. Při `401` klient jednou obnoví přístupový token prostřednictvím hostitele a požadavek znovu odešle. Základní URL a token jsou ve výchozím nastavení odvozeny z prostředí. Podle potřeby předávejte konstruktoru přepsané hodnoty — například v testech: ```ts const client = new RestApiClient({ baseUrl: 'https://api.example.com', token: 'my-token', }); ``` Neúspěšné požadavky vyvolají `RestApiClientError`, který zpřístupňuje `status`, `statusText`, `url` a parsované `body`: ```tsx import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest'; const client = new RestApiClient(); try { const prs = await client.get('/s/github/fetch-prs', { query: { state: 'open' }, }); } catch (error) { if (error instanceof RestApiClientError) { console.error(error.status, error.body); } } ``` ## Přístup k běhovému kontextu Uvnitř komponenty použijte hooky SDK pro přístup k aktuálnímu uživateli, záznamu a instanci komponenty: ```tsx 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 (

User: {userId}

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

Component: {componentId}

); }; export default defineFrontComponent({ universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', name: 'record-info', component: RecordInfo, }); ``` Dostupné hooky: | Hook | Vrací | Popis | | --------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ | | `useUserId()` | `string` nebo `null` | ID aktuálního uživatele | | `useSelectedRecordIds()` | `string[]` | Všechna vybraná ID záznamů (prázdné pole, pokud není nic vybráno) | | `useRecordId()` | `string` nebo `null` | **Zastaralé.** Použijte místo toho `useSelectedRecordIds()` | | `useFrontComponentId()` | `string` | ID této instance komponenty | | `useColorScheme()` | `'light'` nebo `'dark'` | Aktivní barevné schéma uživatelského rozhraní hostitele (`System` je již vyhodnocen) | | `useFrontComponentExecutionContext(selector)` | různé | Přístup k úplnému kontextu běhu pomocí selektorové funkce | ## Aplikační proměnné Aplikační proměnné definované v [`defineApplication()`](/l/cs/developers/extend/apps/config/application) s `isSecret: false` jsou k dispozici ve front-endových komponentách prostřednictvím pomocné funkce `getApplicationVariable`: ```tsx src/front-components/greeting.tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { getApplicationVariable } from 'twenty-sdk/front-component'; const Greeting = () => { const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World'; return

Hello, {recipientName}!

; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'greeting', component: Greeting, }); ``` Tajné proměnné (`isSecret: true`) **nejsou** zpřístupněny front-endovým komponentám. Jsou k dispozici pouze v [logických funkcích](/l/cs/developers/extend/apps/logic/logic-functions), které běží na straně serveru. Tím se zabrání odesílání citlivých hodnot, jako jsou API klíče, do prohlížeče. Následující systémové proměnné jsou vždy dostupné přes `process.env`: | Proměnná | Popis | | ------------------------- | -------------------------------------------------------------- | | `TWENTY_API_URL` | Základní URL Twenty API | | `TWENTY_APP_ACCESS_TOKEN` | Krátkodobý token s oprávněními omezenými na roli vaší aplikace | ## API komunikace s hostitelem Frontendové komponenty mohou pomocí funkcí z `twenty-sdk` vyvolávat navigaci, modály a oznámení: | Funkce | Popis | | ----------------------------------------------- | ------------------------------ | | `navigate(to, params?, queryParams?, options?)` | Přejít na stránku v aplikaci | | `openSidePanelPage(params)` | Otevřít postranní panel | | `closeSidePanel()` | Zavřít postranní panel | | `openCommandConfirmationModal(params)` | Zobrazit potvrzovací dialog | | `enqueueSnackbar(params)` | Zobrazit oznámení typu toast | | `unmountFrontComponent()` | Odpojit komponentu | | `updateProgress(progress)` | Aktualizovat indikátor průběhu | Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce: ```tsx 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 (

Archive this record?

); }; export default defineFrontComponent({ universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', name: 'archive-record', description: 'Archives the current record', component: ArchiveRecord, }); ``` ### Práce s více záznamy Použijte `useSelectedRecordIds()` pro zpracování více vybraných záznamů. To je užitečné pro hromadné operace: ```tsx src/front-components/bulk-export.tsx import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define'; import { useSelectedRecordIds } from 'twenty-sdk/front-component'; import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component'; import { CoreApiClient } from 'twenty-sdk/clients'; 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 (

Export {selectedRecordIds.length} selected record(s)?

); }; export default defineFrontComponent({ universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901', name: 'bulk-export', description: 'Export selected records', component: BulkExport, command: { universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902', label: 'Bulk Export', availabilityType: 'RECORD_SELECTION', conditionalAvailabilityExpression: numberOfSelectedRecords > 0, }, }); ``` ## Veřejné soubory Frontendové komponenty mohou přistupovat k souborům ze složky aplikace `public/` pomocí `getPublicAssetUrl`: ```tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { getPublicAssetUrl } from 'twenty-sdk/utils'; const Logo = () => Logo; export default defineFrontComponent({ universalIdentifier: '...', name: 'logo', component: Logo, }); ``` Podrobnosti viz [sekci veřejných souborů](/l/cs/developers/extend/apps/config/public-assets). ## Stylování Frontendové komponenty podporují více přístupů ke stylování. Můžete použít: * **Inline styly** — `style={{ color: 'red' }}` * **Komponenty Twenty UI** — import z `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar a další) * **Emotion** — CSS-in-JS s `@emotion/react` * **Styled-components** — vzory `styled.div` * **Tailwind CSS** — utilitní třídy * **Jakákoli CSS-in-JS knihovna** kompatibilní s Reactem ```tsx import { defineFrontComponent } from 'twenty-sdk/define'; import { Button, Tag, Status } from 'twenty-sdk/ui'; const StyledWidget = () => { return (
); }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', name: 'styled-widget', component: StyledWidget, }); ```