Zum Hauptinhalt springen
Frontend-Komponenten sind React-Komponenten, die direkt innerhalb der Twenty-UI gerendert werden. Sie laufen in einem isolierten Web Worker unter Verwendung von Remote DOM — Ihr Code wird in einer Sandbox ausgeführt, rendert jedoch nativ auf der Seite, nicht in einem iframe.

Wo Front-Komponenten verwendet werden können

Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
  • Seitenpanel — Nicht-Headless-Front-Komponenten werden im rechten Seitenpanel geöffnet. Dies ist das Standardverhalten, wenn eine Front-Komponente über das Befehlsmenü ausgelöst wird.
  • Widgets (Dashboards und Datensatzseiten) — Front-Komponenten können als Widgets in Seitenlayouts eingebettet werden. Beim Konfigurieren eines Dashboards oder eines Datensatzseiten-Layouts können Benutzer ein Front-Komponenten-Widget hinzufügen.

Einfaches Beispiel

Der schnellste Weg, eine Frontend-Komponente in Aktion zu sehen, ist, sie als Befehl zu registrieren. Das Hinzufügen eines command-Felds mit isPinned: true lässt sie als Schnellaktionsschaltfläche oben rechts auf der Seite erscheinen — kein Seitenlayout erforderlich:
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';

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

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
  command: {
    universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
    shortLabel: 'Hello',
    label: 'Hello World',
    icon: 'IconBolt',
    isPinned: true,
    availabilityType: 'GLOBAL',
  },
});
Nach dem Synchronisieren mit yarn twenty dev (oder durch einmaliges Ausführen von yarn twenty dev --once) erscheint die Schnellaktion oben rechts auf der Seite:
Schnellaktionsschaltfläche oben rechts
Klicken Sie darauf, um die Komponente inline zu rendern.

Konfigurationsfelder

FeldErforderlichBeschreibung
universalIdentifierJaStabile eindeutige ID für diese Komponente
componentJaEine React-Komponentenfunktion
nameNeinAnzeigename
descriptionNeinBeschreibung dessen, was die Komponente macht
isHeadlessNeinAuf true setzen, wenn die Komponente keine sichtbare UI hat (siehe unten)
commandNeinDie Komponente als Befehl registrieren (siehe unten Befehlsoptionen)

Eine Frontend-Komponente auf einer Seite platzieren

Über Befehle hinaus können Sie eine Frontend-Komponente direkt in eine Datensatzseite einbetten, indem Sie sie als Widget in einem Seitenlayout hinzufügen. Details finden Sie im Abschnitt definePageLayout.

Headless vs. Nicht-Headless

Front-Komponenten gibt es in zwei Rendering-Modi, die durch die Option isHeadless gesteuert werden: Nicht-Headless (Standard) — Die Komponente rendert eine sichtbare UI. Wird sie über das Befehlsmenü ausgelöst, öffnet sie sich im Seitenpanel. Dies ist das Standardverhalten, wenn isHeadless false ist oder weggelassen wird. Headless (isHeadless: true) — Die Komponente wird unsichtbar im Hintergrund gemountet. Sie öffnet das Seitenpanel nicht. Headless-Komponenten sind für Aktionen konzipiert, die Logik ausführen und sich anschließend selbst unmounten — zum Beispiel das Ausführen einer asynchronen Aufgabe, das Navigieren zu einer Seite oder das Anzeigen eines Bestätigungsdialogs. Sie lassen sich gut mit den unten beschriebenen SDK-Command-Komponenten kombinieren.
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,
});
Da die Komponente null zurückgibt, überspringt Twenty das Rendern eines Containers dafür — im Layout entsteht kein Leerraum. Die Komponente hat dennoch Zugriff auf alle Hooks und die Host-Kommunikations-API.

SDK-Command-Komponenten

Das Paket twenty-sdk stellt vier Command-Hilfskomponenten bereit, die für Headless-Front-Komponenten ausgelegt sind. Jede Komponente führt beim Mounten eine Aktion aus, behandelt Fehler durch Anzeige einer Snackbar-Benachrichtigung und unmountet die Front-Komponente nach Abschluss automatisch. Importieren Sie sie aus twenty-sdk/command:
  • Command — Führt einen asynchronen Callback über das Prop execute aus.
  • CommandLink — Navigiert zu einem App-Pfad. Props: to, params, queryParams, options.
  • CommandModal — Öffnet einen Bestätigungsdialog. Bestätigt der Benutzer, wird der Callback execute ausgeführt. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Öffnet eine bestimmte Seite im Seitenpanel. Props: page, pageTitle, pageIcon.
Hier ist ein vollständiges Beispiel einer Headless-Front-Komponente, die Command verwendet, um eine Aktion aus dem Befehlsmenü auszuführen:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

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

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

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

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
  command: {
    universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
    label: 'Run my action',
    icon: 'IconPlayerPlay',
  },
});
Und ein Beispiel, das CommandModal verwendet, um vor der Ausführung um Bestätigung zu bitten:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';

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

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

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

Zugriff auf den Laufzeitkontext

Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
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,
});
Verfügbare Hooks:
HookGibt zurückBeschreibung
useUserId()string oder nullDie ID des aktuellen Benutzers
useRecordId()string oder nullDie ID des aktuellen Datensatzes (wenn auf einer Datensatzseite platziert)
useFrontComponentId()ZeichenketteDie ID dieser Komponenteninstanz
useFrontComponentExecutionContext(selector)variiertZugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion

Host-Kommunikations-API

Frontend-Komponenten können Navigation, Modals und Benachrichtigungen mittels Funktionen aus twenty-sdk auslösen:
FunktionBeschreibung
navigate(to, params?, queryParams?, options?)Zu einer Seite in der App navigieren
openSidePanelPage(params)Ein Seitenpanel öffnen
closeSidePanel()Seitenpanel schließen
openCommandConfirmationModal(params)Einen Bestätigungsdialog anzeigen
enqueueSnackbar(params)Eine Toast-Benachrichtigung anzeigen
unmountFrontComponent()Die Komponente entfernen
updateProgress(progress)Einen Fortschrittsindikator aktualisieren
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
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,
});

Befehlsoptionen

Das Hinzufügen eines command-Felds zu defineFrontComponent registriert die Komponente im Befehlsmenü (Cmd+K). Wenn isPinned true ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
FeldErforderlichBeschreibung
universalIdentifierJaStabile eindeutige ID für den Befehl
labelJaVollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird
shortLabelNeinKürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird
iconNeinNeben dem Label angezeigter Icon-Name (z. B. ‘IconBolt’, ‘IconSend’)
isPinnedNeinBei true wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt
availabilityTypeNeinSteuert, wo der Befehl erscheint: ‘GLOBAL’ (immer verfügbar), ‘RECORD_SELECTION’ (nur wenn Datensätze ausgewählt sind) oder ‘FALLBACK’ (wird angezeigt, wenn keine anderen Befehle passen)
availabilityObjectUniversalIdentifierNeinBeschränken Sie den Befehl auf Seiten eines bestimmten Objekttyps (z. B. nur bei Company-Datensätzen)
conditionalAvailabilityExpressionNeinEin boolescher Ausdruck, um dynamisch zu steuern, ob der Befehl sichtbar ist (siehe unten)

Bedingte Verfügbarkeitsausdrücke

Mit dem Feld conditionalAvailabilityExpression können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus twenty-sdk, um Ausdrücke zu erstellen:
import { defineFrontComponent } from 'twenty-sdk/define';
import {
  pageType,
  numberOfSelectedRecords,
  objectPermissions,
  everyEquals,
  isDefined,
} from 'twenty-sdk/front-component';

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'bulk-action',
  component: BulkAction,
  command: {
    universalIdentifier: '...',
    label: 'Bulk Update',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: everyEquals(
      objectPermissions,
      'canUpdateObjectRecords',
      true,
    ),
  },
});
Kontextvariablen — sie repräsentieren den aktuellen Zustand der Seite:
VariableTypBeschreibung
pageTypeZeichenketteAktueller Seitentyp (z. B. ‘RecordIndexPage’, ‘RecordShowPage’)
isInSidePanelbooleanOb die Komponente in einem Seitenpanel gerendert wird
numberOfSelectedRecordsnumberAnzahl der aktuell ausgewählten Datensätze
isSelectAllbooleanOb „Alle auswählen“ aktiv ist
selectedRecordsarrayDie ausgewählten Datensatzobjekte
favoriteRecordIdsarrayIDs der favorisierten Datensätze
objectPermissionsobjectBerechtigungen für den aktuellen Objekttyp
targetObjectReadPermissionsobjectLeseberechtigungen für das Zielobjekt
targetObjectWritePermissionsobjectSchreibberechtigungen für das Zielobjekt
featureFlagsobjectAktive Feature-Flags
objectMetadataItemobjectMetadaten des aktuellen Objekttyps
hasAnySoftDeleteFilterOnViewbooleanOb die aktuelle Ansicht einen Soft-Delete-Filter hat
Operatoren — Variablen zu booleschen Ausdrücken kombinieren:
OperatorBeschreibung
isDefined(value)true, wenn der Wert nicht null/undefined ist
isNonEmptyString(value)true, wenn der Wert eine nicht leere Zeichenfolge ist
includes(array, value)true, wenn das Array den Wert enthält
includesEvery(array, prop, value)true, wenn die Eigenschaft jedes Elements den Wert enthält
every(array, prop)true, wenn die Eigenschaft bei jedem Element truthy ist
everyDefined(array, prop)true, wenn die Eigenschaft bei jedem Element definiert ist
everyEquals(array, prop, value)true, wenn die Eigenschaft bei jedem Element dem Wert entspricht
some(array, prop)true, wenn die Eigenschaft bei mindestens einem Element truthy ist
someDefined(array, prop)true, wenn die Eigenschaft bei mindestens einem Element definiert ist
someEquals(array, prop, value)true, wenn die Eigenschaft bei mindestens einem Element dem Wert entspricht
someNonEmptyString(array, prop)true, wenn die Eigenschaft bei mindestens einem Element eine nicht leere Zeichenfolge ist
none(array, prop)true, wenn die Eigenschaft bei jedem Element falsy ist
noneDefined(array, prop)true, wenn die Eigenschaft bei jedem Element undefined ist
noneEquals(array, prop, value)true, wenn die Eigenschaft bei keinem Element dem Wert entspricht

Öffentliche Assets

Frontend-Komponenten können mit getPublicAssetUrl auf Dateien aus dem public/-Verzeichnis der App zugreifen:
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';

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

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
Details finden Sie im Abschnitt Öffentliche Assets.

Styling

Frontend-Komponenten unterstützen mehrere Styling-Ansätze. Sie können verwenden:
  • Inline-Stylesstyle={{ color: 'red' }}
  • Twenty-UI-Komponenten — Import aus twenty-sdk/ui (Button, Tag, Status, Chip, Avatar und mehr)
  • Emotion — CSS-in-JS mit @emotion/react
  • Styled-componentsstyled.div-Muster
  • Tailwind CSS — Utility-Klassen
  • Beliebige CSS-in-JS-Bibliothek, die mit React kompatibel ist
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,
});