Salt la conținutul principal
Componentele front-end sunt componente React care se afișează direct în interfața Twenty. Rulează într-un Web Worker izolat folosind Remote DOM — codul este izolat (sandboxed), dar se redă nativ în pagină, nu într-un iframe.

Unde pot fi utilizate componentele front-end

Componentele front-end pot fi afișate în două locații în cadrul Twenty:
  • Panou lateral — Componentele front-end care nu sunt headless se deschid în panoul lateral din dreapta. Acesta este comportamentul implicit atunci când o componentă front-end este declanșată din meniul de comenzi.
  • Widgeturi (tablouri de bord și pagini de înregistrare) — Componentele frontale pot fi încorporate ca widgeturi în machetele de pagină. La configurarea unui tablou de bord sau a machetei unei pagini de înregistrare, utilizatorii pot adăuga un widget de componentă front-end.
O componentă frontală, de una singură, nu este accesibilă din interfața utilizatorului — trebuie să o expui. Cele două moduri de a face asta sunt:
  • Asociază-l cu un element de meniu de comenzi — îl înregistrează în meniul de comenzi (Cmd+K) și, opțional, ca acțiune rapidă fixată.
  • Încorporează-l ca widget într-o machetă de pagină — îl plasează pe pagina de detalii a unei înregistrări sau pe un tablou de bord.

Exemplu de bază

Cel mai rapid mod de a vedea o componentă frontală în acțiune este să o asociezi cu un defineCommandMenuItem, astfel încât să apară ca un buton de acțiune rapidă în colțul din dreapta sus al paginii:
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',
});
După sincronizarea cu yarn twenty dev (sau prin rularea comenzii yarn twenty dev --once o singură dată), acțiunea rapidă apare în colțul din dreapta sus al paginii:
Buton de acțiune rapidă în colțul din dreapta sus
Faceți clic pe el pentru a afișa componenta inline.

Câmpuri de configurare

CâmpObligatoriuDescriere
universalIdentifierDaID unic stabil pentru această componentă
componentDaO funcție de componentă React
nameNuNume afișat
descriptionNuDescriere a ceea ce face componenta
isHeadlessNuSetați la true dacă componenta nu are interfață vizibilă (vedeți mai jos)

Plasarea unei componente front-end pe o pagină

Dincolo de comenzi, puteți încorpora o componentă front-end direct într-o pagină de înregistrare adăugând-o ca widget într-un layout de pagină. Vezi Machete de pagină pentru detalii.

Headless vs non-headless

Componentele front-end au două moduri de randare controlate de opțiunea isHeadless: Non-headless (implicit) — Componenta afișează o interfață vizibilă. Când este declanșată din meniul de comenzi, se deschide în panoul lateral. Acesta este comportamentul implicit când isHeadless este false sau omis. Headless (isHeadless: true) — Componenta se montează invizibil în fundal. Nu deschide panoul lateral. Componentele headless sunt concepute pentru acțiuni care execută logică și apoi se demontează — de exemplu, rularea unei sarcini asincrone, navigarea la o pagină sau afișarea unui modal de confirmare. Se potrivesc în mod natural cu componentele Command din SDK descrise mai jos.
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,
});
Deoarece componenta returnează null, Twenty omite redarea unui container pentru ea — nu apare spațiu gol în layout. Componenta are în continuare acces la toate hook-urile și la API-ul de comunicare cu gazda.

Componentele Command din SDK

Pachetul twenty-sdk oferă patru componente ajutătoare Command, concepute pentru componente front-end headless. Fiecare componentă execută o acțiune la montare, gestionează erorile afișând o notificare snackbar și demontează automat componenta front-end la final. Importați-le din twenty-sdk/command:
  • Command — Rulează un callback asincron prin prop-ul execute.
  • CommandLink — Navighează către o rută a aplicației. Props: to, params, queryParams, options.
  • CommandModal — Deschide un modal de confirmare. Dacă utilizatorul confirmă, execută callback-ul execute. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Deschide o anumită pagină din panoul lateral. Props: page, pageTitle, pageIcon.
Iată un exemplu complet de componentă front-end headless care folosește Command pentru a rula o acțiune din meniul de comenzi:
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',
});
Și un exemplu care folosește CommandModal pentru a cere confirmarea înainte de execuție:
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,
});

Apelarea unei funcții logice

Componentele de front rulează în browser într-un Web Worker izolat, în timp ce funcțiile logice rulează pe server. Nu există un apel direct în același proces între cele două — în schimb, o componentă de front apelează o funcție logică prin HTTP. O funcție logică declarată cu httpRouteTriggerSettings este expusă sub endpoint-ul /s/ la ${TWENTY_API_URL}/s\<path>. Componenta ta de front apelează acea rută cu RestApiClient din twenty-client-sdk/rest, care se autentifică folosind TWENTY_APP_ACCESS_TOKEN pe care Twenty îl injectează în worker. RestApiClient este creat exact pentru acest scop. Acesta citește TWENTY_API_URL și TWENTY_APP_ACCESS_TOKEN din mediul worker-ului, atașează antetul Authorization: Bearer, serializează și parsează JSON și aruncă un RestApiClientError atunci când token-ul sau URL-ul lipsesc sau când răspunsul nu este 2xx — astfel încât să nu trebuiască să reimplementezi acel boilerplate în fiecare componentă. O componentă de front headless poate efectua apelul la montare prin componenta Command, apoi se demontează automat:
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,
});
Calea transmisă către client este calea publică a rutei — proprietatea httpRouteTriggerSettings.path a funcției logice, cu prefixul /s. Păstrează isAuthRequired: true; clientul furnizează tokenul de acces al aplicației pe care Twenty îl emite pentru componenta ta:
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 și TWENTY_APP_ACCESS_TOKEN sunt injectate automat — vezi Application variables. Deoarece variabilele de aplicație secrete nu sunt niciodată expuse componentelor de front, păstrează cheile API și altă logică sensibilă în funcția logică, nu în componenta de front.

Referință pentru RestApiClient

Importă RestApiClient din twenty-client-sdk/rest. Face parte din aceeași familie de clienți ca CoreApiClient și MetadataApiClient, dar vizează rutele HTTP ale aplicației tale în locul API-ului GraphQL.
MetodăDescriere
get(path, options?)Trimite o cerere GET
post(path, body?, options?)Trimite o cerere POST
put(path, body?, options?)Trimite o cerere PUT
patch(path, body?, options?)Trimite o cerere PATCH
delete(path, options?)Trimite o cerere DELETE
request(method, path, options?)Cerere generică cu orice metodă HTTP
options acceptă headers, query (un „record” de parametri de query-string; valorile nule sau nedefinite sunt omise) și un AbortSignal prin signal. Un obiect body care nu este de tip FormData este serializat automat în JSON. La un 401, clientul reîmprospătează o dată tokenul de acces prin gazdă și reîncearcă cererea. URL-ul de bază și tokenul sunt rezolvate din mediu în mod implicit. Transmite suprascrieri către constructor atunci când este necesar — de exemplu, în teste:
const client = new RestApiClient({
  baseUrl: 'https://api.example.com',
  token: 'my-token',
});
Cererile eșuate declanșează o eroare RestApiClientError care expune status, statusText, url și body analizat:
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);
  }
}

Accesarea contextului de rulare

În interiorul componentei, folosiți hook-urile SDK pentru a accesa utilizatorul curent, înregistrarea curentă și instanța componentei:
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,
});
Hook-uri disponibile:
HookReturneazăDescriere
useUserId()string sau nullID-ul utilizatorului curent
useSelectedRecordIds()string[]Toate ID-urile înregistrărilor selectate (array gol dacă nu este selectată niciuna)
useRecordId()string sau nullÎnvechit. Folosiți useSelectedRecordIds() în schimb
useFrontComponentId()stringID-ul acestei instanțe de componentă
useColorScheme()'light' sau 'dark'Schema de culori activă a interfeței de utilizator a gazdei (System este deja rezolvat)
useFrontComponentExecutionContext(selector)variazăAccesați întregul context de execuție cu o funcție selector

Variabile de aplicație

Variabilele de aplicație definite în defineApplication() cu isSecret: false sunt disponibile în componentele de interfață prin utilitarul 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,
});
Variabilele secrete (isSecret: true) nu sunt expuse componentelor de interfață. Acestea sunt disponibile doar în funcțiile logice, care rulează pe server. Acest lucru împiedică trimiterea către browser a valorilor sensibile, cum ar fi cheile API.
Următoarele variabile de sistem sunt întotdeauna disponibile prin process.env:
VariabilăDescriere
TWENTY_API_URLURL de bază al API-ului Twenty
TWENTY_APP_ACCESS_TOKENToken cu durată scurtă, limitat la rolul aplicației dvs.

API-ul de comunicare cu gazda

Componentele front-end pot declanșa navigare, ferestre modale și notificări folosind funcții din twenty-sdk:
FuncțieDescriere
navigate(to, params?, queryParams?, options?)Navigați la o pagină din aplicație
openSidePanelPage(params)Deschideți un panou lateral
closeSidePanel()Închideți panoul lateral
openCommandConfirmationModal(params)Afișați un dialog de confirmare
enqueueSnackbar(params)Afișați o notificare tip toast
unmountFrontComponent()Demontați componenta
updateProgress(progress)Actualizați un indicator de progres
Iată un exemplu care folosește API-ul gazdei pentru a afișa un snackbar și a închide panoul lateral după finalizarea unei acțiuni:
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,
});

Lucrul cu mai multe înregistrări

Folosiți useSelectedRecordIds() pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă:
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,
  },
});

Resurse publice

Componentele front-end pot accesa fișiere din directorul public/ al aplicației folosind 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,
});
Consultați secțiunea despre resurse publice pentru detalii.

Stilizare

Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi:
  • Stiluri inlinestyle={{ color: 'red' }}
  • Componente Twenty UI — import din twenty-sdk/ui (Button, Tag, Status, Chip, Avatar și altele)
  • Emotion — CSS-in-JS cu @emotion/react
  • Styled-components — pattern-uri styled.div
  • Tailwind CSS — clase utilitare
  • Orice bibliotecă CSS-in-JS compatibilă cu React
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';

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

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