Přejít na hlavní obsah
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. 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 — 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 — 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, aby se objevila jako tlačítko rychlé akce v pravém horním rohu stránky:
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,
});
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

PolePovinnéPopis
universalIdentifierAnoStabilní jedinečné ID pro tuto komponentu
componentAnoFunkce komponenty React
nameNeZobrazovaný název
descriptionNePopis toho, co komponenta dělá
isHeadlessNeNastavte 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.

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.
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ů:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

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

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

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

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
});
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:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';

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

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

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
});

Volání logické funkce

Front komponenty běží v prohlížeči v izolovaném web workeru, zatímco logické funkce 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\<path>. 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:
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 <Command execute={execute} />;
};

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:
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. 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.
MetodaPopis
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:
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:
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:
src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk/front-component';

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

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

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
Dostupné hooky:
HookVracíPopis
useUserId()string nebo nullID aktuálního uživatele
useSelectedRecordIds()string[]Všechna vybraná ID záznamů (prázdné pole, pokud není nic vybráno)
useRecordId()string nebo nullZastaralé. Použijte místo toho useSelectedRecordIds()
useFrontComponentId()stringID 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() s isSecret: false jsou k dispozici ve front-endových komponentách prostřednictvím pomocné funkce getApplicationVariable:
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,
});
Tajné proměnné (isSecret: true) nejsou zpřístupněny front-endovým komponentám. Jsou k dispozici pouze v logických funkcích, 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_URLZákladní URL Twenty API
TWENTY_APP_ACCESS_TOKENKrá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í:
FunkcePopis
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:
src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';

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

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

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

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:
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 (
    <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,
  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:
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,
});
Podrobnosti viz sekci veřejných souborů.

Stylování

Frontendové komponenty podporují více přístupů ke stylování. Můžete použít:
  • Inline stylystyle={{ 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
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';

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

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