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: Befehlsmenü-Einträge
description: Stellen Sie Front-Komponenten als Schnellaktionen und Einträge im Befehlsmenü (Cmd+K) mit defineCommandMenuItem bereit.
icon: terminal
---
Ein **Befehlsmenü-Eintrag** ist die Brücke zwischen dem Benutzer und einer [Front-Komponente](/l/de/developers/extend/apps/layout/front-components). Er registriert die Komponente im Twenty-Befehlsmenü (Cmd+K) und zeigt sie optional als angeheftete Schnellaktionsschaltfläche oben rechts auf der Seite an.
```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',
});
```
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für den Befehl |
| `label` | Ja | Vollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird |
| `frontComponentUniversalIdentifier` | Ja | Der `universalIdentifier` der Front-Komponente, die dieser Befehl öffnet |
| `shortLabel` | Nein | Kürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird |
| `icon` | Nein | **Veraltet** — wird zugunsten des Anwendungssymbols ignoriert; der Build gibt eine Warnung aus, wenn gesetzt |
| `isPinned` | Nein | Bei `true` wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt |
| `availabilityType` | Nein | Steuert, wo der Befehl erscheint: `'GLOBAL'` (immer verfügbar), `'GLOBAL_OBJECT_CONTEXT'` (nur auf Seiten mit einem Objektkontext – Index- und Datensatzseiten), `'RECORD_SELECTION'` (nur wenn Datensätze ausgewählt sind) oder `'FALLBACK'` (wird angezeigt, wenn keine anderen Befehle passen) |
| `availabilityObjectUniversalIdentifier` | Nein | Beschränken Sie den Befehl auf Seiten eines bestimmten Objekttyps (z. B. nur bei Company-Datensätzen) |
| `conditionalAvailabilityExpression` | Nein | Ein boolescher Ausdruck, der die Sichtbarkeit dynamisch steuert (siehe unten) |
## Headless-Befehle
Ein Befehlsmenü-Eintrag, der mit einer [Headless-Front-Komponente](/l/de/developers/extend/apps/layout/front-components#headless-vs-non-headless) gekoppelt ist, ist die idiomatische Art, eine One-Click-Aktion bereitzustellen – Code ausführen, navigieren oder bestätigen und ausführen. Die Seite „Front Components“ behandelt die [SDK Command-Komponenten](/l/de/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`), die das Action-and-Unmount-Muster handhaben.
Ein typischer Ablauf: Eine Headless-Komponente rendert `<Command execute={...} />` (siehe das [vollständige Beispiel](/l/de/developers/extend/apps/layout/front-components#sdk-command-components)), und der Befehlsmenüeintrag verweist darauf:
```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',
});
```
## 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:
```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` impliziert bereits eine nichtleere Auswahl — verwende `numberOfSelectedRecords` nur für bestimmte Zählwerte (z. B. `>= 2`).
</Note>
### Kontextvariablen
Diese repräsentieren den aktuellen Zustand der Seite:
| Variable | Typ | Beschreibung |
| ------------------------------ | --------- | --------------------------------------------------------------- |
| `pageType` | `string` | Aktueller Seitentyp (z. B. 'RecordIndexPage', 'RecordShowPage') |
| `isInSidePanel` | `boolean` | Ob die Komponente in einem Seitenpanel gerendert wird |
| `numberOfSelectedRecords` | `number` | Anzahl der aktuell ausgewählten Datensätze |
| `isSelectAll` | `boolean` | Ob „Alle auswählen“ aktiv ist |
| `selectedRecords` | `array` | Die ausgewählten Datensatzobjekte |
| `favoriteRecordIds` | `array` | IDs der favorisierten Datensätze |
| `objectPermissions` | `object` | Berechtigungen für den aktuellen Objekttyp |
| `targetObjectReadPermissions` | `object` | Leseberechtigungen für das Zielobjekt |
| `targetObjectWritePermissions` | `object` | Schreibberechtigungen für das Zielobjekt |
| `featureFlags` | `object` | Aktive Feature-Flags |
| `objectMetadataItem` | `object` | Metadaten des aktuellen Objekttyps |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Ob die aktuelle Ansicht einen Soft-Delete-Filter hat |
### Operatoren
Kombinieren Sie Variablen zu booleschen Ausdrücken:
| Operator | Beschreibung |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| `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 |

View File

@@ -0,0 +1,849 @@
---
title: Frontend-Komponenten
description: Erstellen Sie React-Komponenten, die innerhalb der Twenty-UI gerendert werden und durch eine Sandbox isoliert sind.
icon: window-maximize
---
Front-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 einem sandboxed iframe mit opaker Origin ausgeführt, wobei die UI dennoch nativ auf der Seite gerendert wird und nicht auf dieses iframe beschränkt ist.
<Warning>
Front-Komponenten befinden sich noch in aktiver Entwicklung. Ihr Code wird gegen ein teilweises DOM und nicht gegen eine echte Browserseite ausgeführt, sodass fortgeschrittene Anwendungsfälle fehlschlagen können – oft ohne sichtbare Fehlermeldung. Siehe [Aktuelle Einschränkungen](#current-limitations).
</Warning>
## Wo Front-Komponenten verwendet werden können
Front-Komponenten können an drei 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](/l/de/developers/extend/apps/layout/page-layouts) eingebettet werden. Beim Konfigurieren eines Dashboards oder eines Datensatzseiten-Layouts können Benutzer ein Front-Komponenten-Widget hinzufügen.
* **App settings** — Definiert mit [`defineSettingsFrontComponent()`](#custom-settings-component), wird die Front-Komponente als Abschnitt im **Settings**-Tab der App gerendert und ersetzt dabei die standardmäßige Variablenkonfigurationsoberfläche.
Eine Front-Komponente allein ist über die Benutzeroberfläche nicht erreichbar – Sie müssen sie *sichtbar machen*. Die drei Möglichkeiten dafür sind:
* **Mit einem [Befehlsmenüeintrag](/l/de/developers/extend/apps/layout/command-menu-items) verknüpfen** — registriert sie im Befehlsmenü (Cmd+K) und optional als angeheftete Schnellaktion.
* **Als Widget in ein [Seitenlayout](/l/de/developers/extend/apps/layout/page-layouts) einbetten** — platziert es auf der Detailseite eines Datensatzes oder in einem Dashboard.
* **Definiere sie mit [`defineSettingsFrontComponent()`](#custom-settings-component)** — rendert sie als Abschnitt im **Settings**-Tab der App und ersetzt dabei die standardmäßige Variablenkonfigurationsoberfläche.
## Einfaches Beispiel
Die schnellste Möglichkeit, eine Front-Komponente in Aktion zu sehen, besteht darin, sie mit einem [`defineCommandMenuItem`](/l/de/developers/extend/apps/layout/command-menu-items) zu verknüpfen, sodass sie als Schnellaktionsschaltfläche in der oberen rechten Ecke der Seite erscheint:
```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',
});
```
Nach dem Synchronisieren mit `yarn twenty dev` (oder durch einmaliges Ausführen von `yarn twenty apply`) erscheint die Schnellaktion oben rechts auf der Seite:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Schnellaktionsschaltfläche oben rechts" />
</div>
Klicken Sie darauf, um die Komponente inline zu rendern.
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| --------------------- | ------------ | --------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
| `component` | Ja | Eine React-Komponentenfunktion |
| `name` | Nein | Anzeigename |
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
## Eine Front-Komponente auf einer Seite platzieren
Über Befehle hinaus können Sie eine Front-Komponente direkt in eine Datensatzseite einbetten, indem Sie sie als Widget in einem **Seitenlayout** hinzufügen. Details finden Sie unter [Seitenlayouts](/l/de/developers/extend/apps/layout/page-layouts).
## Benutzerdefinierte Einstellungen-Komponente
Um die automatisch generierte Variablenkonfigurationsoberfläche im **Settings**-Tab deiner App durch deine eigene Komponente zu ersetzen, definiere sie mit `defineSettingsFrontComponent` statt mit `defineFrontComponent`. Es verwendet dieselben [Konfigurationsfelder](#configuration-fields) (mit Ausnahme von `isHeadless`, das nicht akzeptiert wird, da eine Einstellungskomponente immer eine sichtbare Benutzeroberfläche rendert) und kennzeichnet die Komponente zusätzlich als Einstellungen-UI der App.
Die Komponente wird als Abschnitt innerhalb des **Settings**-Tabs gerendert, nicht als Ersatz für den gesamten Tab. Die von Twenty verwalteten Systembereiche – Auto-Upgrade, App-URL und Verbindungen – werden immer darüber gerendert und können von der App nicht überschrieben werden.
```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,
});
```
Pro App ist nur eine Einstellungs-Frontkomponente zulässig; wenn mehr als eine deklariert wird, schlägt der Build fehl. Wenn vorhanden, rendert der **Settings**-Tab der App diese Komponente anstelle der standardmäßigen Konfigurationsoberfläche für Variablen.
## 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.
```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,
});
```
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/front-component`:
* **`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 Seite im Seitenpanel. Props hängen von `page` ab — z. B. verwendet `ViewRecord` `recordId` + `objectNameSingular` (plus eine optionale `tab`-ID, um den Datensatz auf einem bestimmten Tab zu öffnen), andere Seiten verwenden `pageTitle` + `pageIcon`.
Hier ist ein vollständiges Beispiel einer Headless-Front-Komponente, die `Command` verwendet, um eine Aktion aus dem Befehlsmenü auszuführen:
```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',
});
```
Und ein Beispiel, das `CommandModal` verwendet, um vor der Ausführung um Bestätigung zu bitten:
```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,
});
```
Und ein Beispiel mit `CommandOpenSidePanelPage`, um den aktuellen Datensatz im Seitenbereich auf einem bestimmten Tab zu öffnen. `tab` ist eine Seitenlayout-Tab-ID (Standardlayouts verwenden IDs wie `company-tab-emails` oder `company-tab-timeline`; benutzerdefinierte Layouts verwenden die eigene ID des Tabs). Wenn die ID im Layout des Datensatzes nicht vorhanden ist, wird stattdessen der Standard-Tab geöffnet:
```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,
});
```
## Aufrufen einer Logikfunktion
Front-Komponenten laufen browserseitig in einem Web-Worker, der in einem sandboxed iframe mit opaker Origin ausgeführt wird, während [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) serverseitig laufen. Es gibt keinen direkten In-Process-Aufruf zwischen beiden – stattdessen ruft eine Front-Komponente eine Logikfunktion über HTTP auf.
Eine mit `httpRouteTriggerSettings` deklarierte Logikfunktion ist über HTTP unter ihrem Routenpfad erreichbar. `RestApiClient` behandelt Pfade, die mit `/s/` beginnen, als App-Routen, löst sie zur URL auf, unter der deine Funktionen bereitgestellt werden, und authentifiziert sie mit `TWENTY_APP_ACCESS_TOKEN`.
> **In Twenty Cloud werden HTTP-ausgelöste Logikfunktionen auf einer eigenen, arbeitsbereichsspezifischen Domain bereitgestellt** unter `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Für externe Aufrufer kopiere die exakte URL aus den **HTTP trigger**-Einstellungen der Funktion oder aus dem **Settings**-Tab der Anwendung.
Eine headless Front-Komponente kann den Aufruf beim Mounten über die `Command`-Komponente ausführen und sich anschließend automatisch unmounten:
```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,
});
```
Der an `RestApiClient` übergebene Pfad ist der `httpRouteTriggerSettings.path` der Logikfunktion, der mit `/s` präfixiert ist. Belasse `isAuthRequired: true`; das `TWENTY_APP_ACCESS_TOKEN`, das Twenty für deine Komponente ausstellt, authentifiziert die Anfrage:
```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` wird automatisch injiziert – siehe [Anwendungsvariablen](#application-variables). Da geheime Anwendungsvariablen niemals in Front-Komponenten offengelegt werden, sollten API-Schlüssel und andere sensible Logik in der Logikfunktion verbleiben und nicht in der Front-Komponente.
</Note>
### Aufrufen der Twenty-REST-API
Um App-HTTP-Routen aufzurufen oder Twenty-Datensätze aus einer Front-Komponente zu lesen und zu schreiben, verwende `RestApiClient` aus `twenty-client-sdk/rest`. Es sendet `/s/...`-Pfade an die Funktions-Basis-URL deines Arbeitsbereichs und alle anderen Pfade, einschließlich `/rest/...`, an `TWENTY_API_URL`.
Es handelt immer als die Person, die die Seite betrachtet. `runAs: 'application'` ist nur eine Option für Logikfunktionen: Eine Komponente erhält niemals das eigene Token Ihrer Anwendung. Wenn Sie es hier anfordern, wird ein Fehler ausgelöst. Lagern Sie Arbeiten, die den eigenen Zugriff der Anwendung benötigen, hinter eine Logikfunktion aus und rufen Sie stattdessen diese auf.
| Methode | Beschreibung |
| --------------------------------- | ---------------------------------------------------------------------------------------- |
| `get(path, options?)` | Sendet eine `GET`-Anfrage |
| `post(path, body?, options?)` | Sendet eine `POST`-Anfrage |
| `put(path, body?, options?)` | Sendet eine `PUT`-Anfrage |
| `patch(path, body?, options?)` | Sendet eine `PATCH`-Anfrage |
| `delete(path, options?)` | Sendet eine `DELETE`-Anfrage |
| `request(method, path, options?)` | Generische Anfrage mit einer beliebigen HTTP-Methode |
| `resolveUrl(path, options?)` | Löst einen Pfad zu seiner vollständigen URL auf, ohne eine Anfrage zu senden (für Links) |
`options` akzeptiert `headers`, `query` (ein Record von Query-String-Parametern; null- bzw. undefined-Werte werden übersprungen) sowie ein `AbortSignal` über `signal`. Ein `body`-Objekt, das kein `FormData` ist, wird automatisch als JSON serialisiert. Bei einem `401` aktualisiert der Client das Access-Token einmal über den Host und versucht die Anfrage erneut.
Die Basis-URL und das Token werden standardmäßig aus der Umgebung ermittelt. Gib bei Bedarf – zum Beispiel in Tests – Überschreibungen an den Konstruktor weiter:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Fehlgeschlagene Anfragen lösen einen `RestApiClientError` aus, der `status`, `statusText`, `url` und den geparsten `body` bereitstellt:
```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);
}
}
```
## Zugriff auf den Laufzeitkontext
Verwenden Sie innerhalb Ihrer Komponente SDK-Hooks, um auf den aktuellen Benutzer, den Datensatz und die Komponenteninstanz zuzugreifen:
```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,
});
```
Verfügbare Hooks:
| Hook | Gibt zurück | Beschreibung |
| --------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------- |
| `useUserId()` | `string` oder `null` | Die ID des aktuellen Benutzers |
| `useSelectedRecordIds()` | `string[]` | Alle ausgewählten Datensatz-IDs (leeres Array, wenn keine ausgewählt sind) |
| `useRecordId()` | `string` oder `null` | **Veraltet.** Verwenden Sie stattdessen `useSelectedRecordIds()` |
| `useFrontComponentId()` | `string` | Die ID dieser Komponenteninstanz |
| `useTimelineActivityId()` | `string` oder `null` | Die aktuelle Timeline-Aktivitäts-ID beim Rendern einer benutzerdefinierten Timeline-Zeile |
| `useColorScheme()` | `'light'` oder `'dark'` | Das aktive Farbschema der Host-UI (`System` ist bereits aufgelöst) |
| `useFrontComponentExecutionContext(selector)` | variiert | Zugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion |
## Anwendungsvariablen
In [`defineApplication()`](/l/de/developers/extend/apps/config/application) mit `isSecret: false` definierte Anwendungsvariablen sind in Front-Komponenten über das Hilfsprogramm `getApplicationVariable` verfügbar:
```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>
Geheime Variablen (`isSecret: true`) werden **nicht** in Front-Komponenten offengelegt. Sie sind nur in [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) verfügbar, die serverseitig ausgeführt werden. Dadurch wird verhindert, dass sensible Werte wie API-Schlüssel an den Browser gesendet werden.
</Warning>
`getApplicationVariable` gibt immer einen **String** (oder `undefined`) zurück, unabhängig vom deklarierten `type` der Variable. Der String wird je nach Typ konsistent serialisiert (boolesche Werte als `"true"` / `"false"`, Zahlen als Dezimalstrings, Arrays / Objekte als JSON), im selben Format, das für die Logikfunktion `process.env` verwendet wird — parsen Sie ihn selbst (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Siehe [Variablentypen](/l/de/developers/extend/apps/config/application#variable-types).
Die folgenden Systemvariablen sind immer über `process.env` verfügbar:
| Variable | Beschreibung |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | Basis-URL der Twenty-Core-API |
| `TWENTY_APP_ACCESS_TOKEN` | Kurzlebiges Token, das auf die Schnittmenge der Rolle der angemeldeten Person und der Rolle Ihrer Anwendung beschränkt ist, sodass eine Komponente niemals mehr tun kann als die Person, die sie betrachtet |
### `TWENTY_FUNCTIONS_URL`
Twenty injiziert außerdem `TWENTY_FUNCTIONS_URL` in Front-Komponenten und Logikfunktionen: die Basis-URL, unter der die HTTP-ausgelösten Logikfunktionen deiner App bereitgestellt werden.
Sie existiert, weil diese URL nicht immer der Twenty-Server selbst ist. In Twenty Cloud werden App-Routen auf einer eigenen, arbeitsbereichsspezifischen Domain bereitgestellt (`https://\<your-workspace-subdomain>.withtwenty.com` oder die primäre öffentliche Domain der Anwendung, falls eine konfiguriert ist), damit von der App erzeugte Antworten in einem isolierten Origin und nicht im Origin der Twenty-App ausgeführt werden. Self-Hosted- und lokale Instanzen stellen App-Routen unter dem Präfix `/s` auf dem Server selbst bereit und setzen die Variable eventuell gar nicht. Da die Basis-URL je nach Arbeitsbereich und Instanz variiert, kann dein Code sie nicht hardcoden – der Server injiziert zur Laufzeit den richtigen Wert.
Du musst sie nur selten direkt lesen. Rufe deine Routen über `RestApiClient` mit einem mit `/s/` präfixierten Pfad auf, und der Client löst die URL für dich auf: Er entfernt das Präfix `/s` und verwendet `TWENTY_FUNCTIONS_URL` als Ziel, mit Fallback auf `\<TWENTY_API_URL>/s`, wenn die Variable nicht gesetzt ist. Verwende `resolveUrl('/s/\<path>')`, um die absolute URL ohne das Senden einer Anfrage zu erhalten, z. B. für einen Link. Lies die Variable nur direkt aus, wenn du eine URL manuell zusammenbaust:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## Host-Kommunikations-API
Front-Komponenten können Navigation, Modals und Benachrichtigungen mittels Funktionen aus `twenty-sdk` auslösen:
| Funktion | Beschreibung |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Lädt ein `Blob` in ein FILES-Feld hoch; gibt entweder `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` oder `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` zurück |
Hier ist ein Beispiel, das die Host-API verwendet, um nach Abschluss einer Aktion eine Snackbar anzuzeigen und das Seitenpanel zu schließen:
```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,
});
```
### Speicher
`localStorage` und `sessionStorage` funktionieren wie auf einer normalen Seite mit der standardmäßigen synchronen API. Deine Schlüssel sind auf die Installation deiner App und die angemeldete Benutzerin bzw. den angemeldeten Benutzer beschränkt: Keine andere App kann sie lesen, und wenn sich eine andere Benutzerin oder ein anderer Benutzer im selben Browser anmeldet, beginnt diese Person mit einem leeren Speicher. In `localStorage` geschriebene Werte bleiben geräteweit über Neuladungen hinweg erhalten; `sessionStorage` besteht für die Dauer der Browsersitzung.
```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 speichert die Werte im Namen deiner App, sodass ein Schreibvorgang lokal sofort angewendet und im Hintergrund gespeichert wird. Lesevorgänge warten niemals auf den Host. Es wird nichts synchronisiert: Die Werte folgen der Benutzerin oder dem Benutzer nicht in einen anderen Browser oder auf eine andere Maschine. Verwende daher den [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store) einer Logic Function für alles, was einen Gerätewechsel überstehen muss.
Schreibvorgänge sind begrenzt, und alle Limits zählen Zeichen statt Bytes: Schlüssel sind höchstens 512 Zeichen lang, ein einzelner Wert höchstens 262.144 Zeichen, und jeder Speicher höchstens 1.048.576 Zeichen pro App und Benutzerin oder Benutzer. Ein Schreibvorgang, der ein Limit überschreitet, löst einen `QuotaExceededError` aus, wie bei der Browser-API.
### Mit mehreren Datensätzen arbeiten
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
```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,
});
```
Machen Sie es über einen [Befehlsmenüeintrag](/l/de/developers/extend/apps/layout/command-menu-items) sichtbar, der auf die Auswahl von Datensätzen beschränkt ist:
```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',
});
```
## Öffentliche Assets
Front-Komponenten können mit `getPublicAssetUrl` auf Dateien aus dem `public/`-Verzeichnis der App zugreifen:
```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,
});
```
Details finden Sie im Abschnitt [Öffentliche Assets](/l/de/developers/extend/apps/config/public-assets).
## Freigabe von Abhängigkeiten über die Front-Komponenten
Standardmäßig bündelt jede Front-Komponente ihre eigene Kopie der von ihr importierten Bibliotheken, so dass eine App mit fünf Komponenten fünf Mal React verschickt. Erklären Sie geteilte Abhängigkeiten im Paket Ihrer App. son\`, um diese Bibliotheken einmal zu erstellen und jede Komponente der App aus einer einzigen zwischengespeicherten Datei laden zu lassen:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Jede Komponente importiert dann ihre Abhängigkeiten genau wie zuvor — nichts ändert sich im Komponentencode:
```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,
});
```
Ein paar Dinge, die Sie wissen sollten:
* **Ein freigegebenes Abhängigkeitsbündel pro App.** Das Paket wird aus den eigenen Abhängigkeiten deiner App aufgebaut, so dass du die volle Kontrolle über die von dir ausgelieferten Versionen behalten kannst.
* **Listet die exakten Angaben, die du importierst, auf.** `twenty-ui/input` und `twenty-ui/display` sind zwei Einträge; ein Paketname allein deckt seine Unterpfade nicht ab. Das Auflisten von `react` deckt automatisch `react/jsx-runtime` ab.
* **Teile `react-dom/client` neben `react`.** Jede Komponente wird durch `createRoot` erzeugt, so dass es herauskommt, dass jede Komponente noch React DOM-Pakete enthält.
* \*\*Das Bundle wird zwischengespeichert. \* Es wird unter einer Content-Hash-URL mit einem langlebigen unveränderlichen Cache versorgt so dass es einmal heruntergeladen und über alle Komponenten der App wiederverwendet wird, bis sich eine seiner Abhängigkeiten ändert.
* **Komponenten, die keines der gemeinsam genutzten Pakete importieren, laden sie nie herunter.**
## Styling
Front-Komponenten unterstützen mehrere Styling-Ansätze. Sie können verwenden:
* **Inline-Styles** — `style={{ color: 'red' }}`
* **Twenty UI-Komponenten** — Twentys eigene Komponentenbibliothek; siehe [Verwenden von Twenty UI-Komponenten](#using-twenty-ui-components) unten
* **Emotion** — CSS-in-JS mit `@emotion/react`
* **Styled-components** — `styled.div`-Muster
* **Tailwind CSS** — Utility-Klassen
* **Beliebige CSS-in-JS-Bibliothek**, die mit React kompatibel ist
## Verwenden von Twenty UI-Komponenten
Twenty liefert seine Komponentenbibliothek als [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)-Paket aus. Frontend-Komponenten können sie für Schaltflächen, Tags, Status-Pills, Chips, Avatare, Icons, Typografie und Theme-Tokens verwenden, die automatisch zum hellen und dunklen Theme des Arbeitsbereichs passen.
### Installation
Fügen Sie das Paket zu Ihrer App hinzu und fixieren Sie es auf die Version, mit der Ihre Twenty-Instanz ausgeliefert wird:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` wird zur Build-Zeit in Ihre Frontend-Komponente gebündelt, sodass es nur eine Abhängigkeit Ihrer App sein muss – zur Laufzeit muss nichts konfiguriert werden.
### Komponenten importieren
Importieren Sie aus dem passenden Subpfad statt aus dem Paket-Root, damit nur die Komponenten, die Sie verwenden, in Ihrem Bundle landen:
| Subpfad | Was exportiert wird |
| --------------------------- | ------------------------------------------------- |
| `twenty-ui/input` | `Button`- und Formulareingaben |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` und mehr |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` und mehr |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` und mehr |
| `twenty-ui/icon` | `Icon*`-Komponenten (z. B. `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,
});
```
### Icons
Importieren Sie einzelne Icons aus `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Jedes benannte Icon wird per Tree Shaking berücksichtigt, sodass der Import einiger weniger Ihr Bundle nur gering vergrößert. Vermeiden Sie `IconsProvider`, `useIcons` und `iconsState` – sie ziehen das vollständige Tabler-Icon-Set (mehrere MB) mit hinein.
### Theming und Theme-Tokens
Twenty UI-Komponenten passen sich automatisch dem hellen und dunklen Theme des Arbeitsbereichs an – der Renderer wendet das aktive Farbschema auf dem Host an und die Komponenten leiten ihre Farben daraus ab.
Um dieselben Design-Tokens in Ihren eigenen Inline-Styles zu verwenden, rufen Sie den Hook `useTheme()` auf. Er gibt die Theme-Tokens von Twenty (Abstände, Farben, Radien, Schriftarten) zurück, die mit dem aktiven Theme verknüpft sind – ohne dass ein `ThemeProvider` in Ihrer Komponente eingerichtet werden muss:
```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>
);
};
```
Da `useTheme()` ein Hook ist, lesen Sie Tokens im Komponenten-Body aus, sodass die Werte immer das aktuelle Theme widerspiegeln. Dieselbe Token-Map wird auch als Konstante `themeCssVariables` exportiert, aber bevorzugen Sie `useTheme()` in Frontend-Komponenten – eine modulweite Konstante, die `themeCssVariables` dereferenziert, kann undefiniert sein, während das App-Manifest extrahiert wird.
Um explizit nach dem aktiven Schema zu verzweigen, lesen Sie es mit `useColorScheme()` aus `twenty-sdk/front-component` aus; der Hook gibt 'light' oder 'dark' zurück.
## Aktuelle Einschränkungen
Front-Komponenten befinden sich in aktiver Entwicklung. Rendern, Styling, Ereignisbehandlung, Messen von Elementen und Browser-Speicher funktionieren gut. Alles, was *über* diese Aspekte hinausgeht (Aufrufen einer DOM-Methode auf einem Ref, Beobachten von Größenänderungen eines Elements, Portaling außerhalb deines Baums), fehlt heute noch oder ist unvollständig, und das meiste davon schlägt stillschweigend fehl: keine Exception und auch kein TypeScript-Fehler, da das Gerüst gegen das vollständige Browser-DOM typisiert ist.
Wenn dich eines dieser Probleme blockiert, [eröffne ein Issue](https://github.com/twentyhq/twenty/issues/new/choose), damit es priorisiert wird.
### Layout und Messung
Elemente können sich selbst messen: Der Host spiegelt die Geometrie in die Sandbox, sodass Lesezugriffe lokal beantwortet werden, aber bis zu einem Frame veraltet sein können, und der erste Lesezugriff auf ein noch nie gemessenes Element gibt Nullen zurück. Nach dem Schreiben erneut in einem `requestAnimationFrame`-Callback oder in einem Effekt lesen.
| API | Was passiert |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funktionieren über den Spiegel; das Zuweisen von `scrollTop` / `scrollLeft` ist ein No-Op |
| `getClientRects()`, `window.matchMedia()` | Löst eine Exception aus |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funktionieren und melden den **Browser-Viewport**; die eigene Größe deines Widgets ist `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Gibt nur die Inline-Styles des Elements zurück, niemals die vom Host berechnete Kaskade |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof`-Guards funktionieren) |
| `MutationObserver` | Funktioniert, einschließlich `subtree`, `attributeFilter`, alter Werte und `takeRecords()` |
Positionierung über `getBoundingClientRect` funktioniert jetzt, aber alles, was Größenänderungen über `ResizeObserver` beobachtet (Recharts `ResponsiveContainer`, Floating UIs `autoUpdate`), funktioniert weiterhin nicht. Verwende ohnehin CSS für das Layout: dein Stylesheet erreicht die echte Seite, daher verhalten sich Flexbox, Grid, `aspect-ratio`, `clamp()` und `@container` normal, ohne Frame-Lag.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` und `queueMicrotask` funktionieren ohne das Präfix `window.`. Nur `window.requestAnimationFrame(...)` und ähnliche Aufrufe lösen eine Exception aus.
</Note>
### DOM-Zugriff
Ein `ref` gibt dir ein Sandbox-Element, kein `HTMLElement`.
| Was du schreibst | Was passiert | Stattdessen verwenden |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Löst eine Exception aus | Gesteuerte Komponenten; Werte aus `event.target` auslesen |
| `element.classList.add(...)` | Löst eine Exception aus (`classList` ist `undefined`) | Erstelle den `className`-String selbst |
| `document.createTreeWalker()` | Löst eine Exception aus | `querySelector()` / `querySelectorAll()` und `getElementById()` funktionieren; `getElementsByClassName()` funktioniert ebenfalls, gibt aber eine statische Collection zurück, keine Live-`HTMLCollection` |
| `document.activeElement` | Immer `undefined` | Fokus mit `onFocus` / `onBlur` verfolgen |
| `canvas` | Rendert nichts, keine Fehlermeldung | SVG oder außerhalb des Bildschirms zeichnen und das Ergebnis in einem `img`-Element anzeigen |
| `createPortal(node, document.body)` | Rendert nichts, während `isConnected` Erfolg meldet | Overlays inline mit `position: absolute` oder der Bibliothek dein eigenes Container-Element übergeben |
Diese Portal-Lücke ist der Grund, warum Radix-, Headless-UI-, MUI- und react-select-Popover standardmäßig nichts rendern. Die meisten akzeptieren eine Container-Prop; verweise sie auf ein Element, das du gerendert hast.
### Ereignisse
Maus, Zeiger, Touch, Drag, Tastatur, Fokus, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` und `animationend`/`transitionend` werden an den Host durchgereicht, plus ein paar pro Element: `load`/`error` auf `img`, Zwischenablage und Komposition auf `input`/`textarea`, Medien auf `video`/`audio`, `toggle` auf `details`/`dialog`. Alles andere (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, Pointer-Capture, `onLoad` von `img`) wird ohne Warnung verworfen.
`document.addEventListener()` und `window.addEventListener()` werden ohne Fehler registriert und feuern nie, weshalb ein Drag aufhört, sobald der Zeiger das Element verlässt, auf dem er begonnen hat. `event.preventDefault()` wird ebenfalls nicht durchgereicht; Formularübermittlung, `dragover`/`drop` und Link-Klicks sind bereits für dich abgesichert.
### Attribute und Styling
Jedes Element leitet seine eigenen Eigenschaften an den Host-DOM weiter (`href` auf `a`, `src`/`alt` auf `img`, `value`/`placeholder`/`disabled` auf `input` usw.), plus einen gemeinsamen Satz auf jedem Element: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` und jedes `aria-*`-/`data-*`-Attribut (mit Bindestrich, daher wird `ariaLabel` verworfen). Alles außerhalb davon wird stillschweigend verworfen, daher solltest du benutzerdefinierte Zustände als `data-*` ausdrücken.
Component-CSS, egal ob aus `import './styles.css'`, CSS-in-JS oder einem `style`-Element, wird **unscoped** in den `head` der Host-Seite injiziert. Dadurch kollidieren Klassennamen mit denen von Twenty (präfixe sie und schreibe niemals nacktes `div { ... }`-Selektoren), und `@media` bezieht sich auf das Browserfenster statt auf dein Widget (verwende `@container` mit deinem eigenen `container-type`). Inline-`style`-Props sind nicht betroffen.
### Speicher und Netzwerk
`localStorage` und `sessionStorage` werden von Twenty statt vom Browser bereitgestellt: Die Komponente läuft in einem Worker mit einem intransparenten Ursprung, daher speichert der Host die Werte im Namen deiner App. Siehe [Speicher](#storage) für ihren Geltungsbereich und ihre Limits. IndexedDB, Cookies, die Cache API und `BroadcastChannel` sind weiterhin nicht verfügbar. Um Zustand geräteübergreifend zu persistieren, rufe eine [Logic Function](/l/de/developers/extend/apps/logic/logic-functions) auf und verwende deren [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store).
`fetch` funktioniert, mit Einschränkungen:
* Aufrufe an die Twenty-API und die Routen deiner App werden vom Host als Proxy weitergeleitet, daher solltest du [`RestApiClient`](#calling-the-twenty-rest-api) bevorzugen. Bei proxied Aufrufen werden `AbortSignal` und die anderen `RequestInit`-Optionen verworfen, und es werden nur `string`- und `URLSearchParams`-Bodies unterstützt.
* Andere Origins verlassen die Sandbox mit `Origin: null`, sodass eine Drittanbieter-API nur antwortet, wenn sie `Access-Control-Allow-Origin: *` sendet. Rufe sie stattdessen aus einer Logic Function auf.
* `fetch('/rest/people')` wird niemals der Twenty-API zugeordnet, weil die Sandbox keine Seiten-URL hat, gegen die ein relativer Pfad aufgelöst werden könnte.
### Medienerfassung
`navigator.mediaDevices.getUserMedia()` und `MediaRecorder` funktionieren innerhalb von Front-Komponenten über Sandbox-Polyfills, sodass Standard-Aufnahmecode unverändert ausgeführt wird und `MediaRecorder.isTypeSupported` Auskunft darüber gibt, ob gängige Container-/Codec-Kombinationen unterstützt werden. Detaillierte `getUserMedia`-Constraint-Objekte werden akzeptiert, aber nicht weitergereicht – der Host erfasst mit seinen Standardwerten für die angeforderten Typen – und es kann immer nur eine Aufnahme gleichzeitig über Anwendungen hinweg aktiv sein. Speichere ein aufgezeichnetes `Blob` mit der Host-Funktion `uploadFile`.
### Weitere Lücken
* **Dateiinhalt.** Ein `input` des Typs `file` gibt deinem Handler nur Dateimetadaten, nicht die Bytes, daher ist `FileReader` nicht verfügbar. Um ein `Blob` hochzuladen, das dein Code bereits besitzt – z. B. eines, das von `MediaRecorder` erzeugt wurde – verwende die Host-Funktion `uploadFile`.
* **Drag-and-drop-Nutzlasten.** Drag-Events werden ausgelöst, aber `event.dataTransfer` ist `undefined`.
* **Node-Built-ins.** `fs`, `path` und `node:crypto` führen zu einem Build-Fehler, daher solltest du diese Arbeit in eine [Logic Function](/l/de/developers/extend/apps/logic/logic-functions) auslagern. Web Crypto, `fetch`, `TextEncoder` und `URL` sind verfügbar.
* **`iframe`** wird immer erneut ohne `allow-same-origin` in einer Sandbox ausgeführt, sodass eine Einbettung, die auf ihre eigene Sitzung angewiesen ist, als abgemeldet gerendert wird. Es hat auch kein `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Navigationsmenüeinträge
description: Fügen Sie der Arbeitsbereichsseitenleiste benutzerdefinierte Einträge hinzu – Links zu gespeicherten Ansichten oder externen URLs.
icon: bars
---
Ein **Navigationsmenüeintrag** ist ein Eintrag in der linken Seitenleiste. Verwenden Sie `defineNavigationMenuItem()`, um benutzerdefinierte Seitenleistenlinks bereitzustellen – typischerweise einen pro [Ansicht](/l/de/developers/extend/apps/layout/views), die Sie bereitstellen – oder um auf externe URLs zu verweisen.
```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,
});
```
## Hauptpunkte
* `type` legt fest, worauf der Menüeintrag verweist. Jeder Typ ist einem bestimmten Bezeichnerfeld zugeordnet:
| Typ | Was es tut | Pflichtfeld |
| ------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Öffnet eine gespeicherte Ansicht | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Öffnet eine externe URL | `link` |
| `NavigationMenuItemType.FOLDER` | Gruppiert verschachtelte Einträge unter einer Bezeichnung | `name` (und untergeordnete Einträge verweisen über `folderUniversalIdentifier` auf den Ordner) |
| `NavigationMenuItemType.OBJECT` | Öffnet die Standardindexseite eines Objekts | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Öffnet ein eigenständiges Seitenlayout | `pageLayoutUniversalIdentifier` |
* `position` steuert die Reihenfolge in der Seitenleiste.
* Das Enum enthält außerdem `NavigationMenuItemType.RECORD`, das intern für vom Benutzer erstellte Datensatzfavoriten verwendet wird — es ist in einem App-Manifest nicht verwendbar (es gibt kein Feld, um auf einen Datensatz zu verweisen).
* `icon` und `color` sind optional und passen das Erscheinungsbild des Eintrags an.
* `folderUniversalIdentifier` ist ebenfalls bei jedem Eintrag verfügbar, um ihn innerhalb eines übergeordneten Elements vom Typ `FOLDER` zu verschachteln.
<Note>
**Häufige Falle:** Wenn Sie ein Objekt ohne zugehörigen Navigationsmenüeintrag erstellen, ist dieses Objekt von der Seitenleiste aus nicht erreichbar. Sofern es sich nicht um ein technisches/internes Objekt handelt, sollte jedes benutzerdefinierte Objekt einen entsprechenden Eintrag in der Seitenleiste haben, der darauf verweist.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Übersicht
description: Binden Sie Ihre App in das UI von Twenty ein – Seitenleisten-Einträge, gespeicherte Ansichten, Registerkarten auf Datensatzseiten und isolierte React-Komponenten.
icon: table-columns
---
Die **Layout-Ebene** einer Twenty-App umfasst alles, was der Benutzer sieht: wo die App in der Seitenleiste erscheint, welche Listenansichten sie bereitstellt, wie ihre Detailseiten für Datensätze angeordnet sind und welche benutzerdefinierten React-Komponenten innerhalb dieser Seiten gerendert werden.
```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 └─────────────────────┘
```
## In diesem Abschnitt
<CardGroup cols={2}>
<Card title="Ansichten" icon="list" href="/l/de/developers/extend/apps/layout/views">
`defineView` — gespeicherte Listen-Konfigurationen: sichtbare Spalten, Filter, Gruppen.
</Card>
<Card title="Navigationsmenüeinträge" icon="bars" href="/l/de/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — Seitenleisten-Einträge, die auf Ansichten oder externe URLs verweisen.
</Card>
<Card title="Seitenlayouts" icon="table-columns" href="/l/de/developers/extend/apps/layout/page-layouts">
`definePageLayout` und `definePageLayoutTab` — Registerkarten und Widgets auf der Detailseite eines Datensatzes.
</Card>
<Card title="Frontend-Komponenten" icon="window-maximize" href="/l/de/developers/extend/apps/layout/front-components">
`defineFrontComponent` — isolierte React-Komponenten, die innerhalb von Twenty gerendert werden.
</Card>
<Card title="Befehlsmenü-Einträge" icon="terminal" href="/l/de/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — Frontend-Komponenten als Cmd+K-Einträge und Schnellaktionen registrieren.
</Card>
</CardGroup>
## Wo die App erscheint
| Oberfläche | Was es steuert | Entität |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Seitenleiste** | Ein benutzerdefinierter Eintrag, der auf eine gespeicherte Ansicht oder eine externe URL verweist | `defineNavigationMenuItem` |
| **Datensatzliste** | Eine gespeicherte Konfiguration für ein Objekt – sichtbare Spalten, Reihenfolge, Filter, Gruppen | `defineView` |
| **Detailseite des Datensatzes** | Die Registerkarten und Widgets auf einer Datensatzseite (für Ihr eigenes Objekt oder ein Standardobjekt) | `definePageLayout`, `definePageLayoutTab` |
| **Innerhalb eines der oben genannten Bereiche** | Ein benutzerdefiniertes React-Widget – Schaltflächen, Formulare, Dashboards, Integrationen | `defineFrontComponent` |
| **App-Einstellungen** | Ein benutzerdefinierter Konfigurationsabschnitt im Tab "Einstellungen" der App, anstelle der standardmäßigen Variablen-Benutzeroberfläche | `defineSettingsFrontComponent` |
| **Befehlsmenü (Cmd+K)** | Eine angeheftete Schnellaktion oder ein versteckter Befehl | `defineCommandMenuItem` |
Frontend-Komponenten laufen in einem isolierten Web Worker unter Verwendung von Remote DOM – sie werden nativ auf der Seite gerendert (nicht in einem iframe), können aber die Hostseite oder das DOM nicht direkt erreichen. Die Kommunikation mit Twenty erfolgt über eine Message-Passing-Host-API.
Sie befinden sich noch in aktiver Entwicklung: Die Sandbox implementiert ein partielles DOM, daher können fortgeschrittene Anwendungsfälle fehlschlagen. Siehe [Aktuelle Einschränkungen](/l/de/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Seitenlayouts
description: Passen Sie Detailseiten von Datensätzen an – Tabs, Widgets und die Stellen, an denen Frontend-Komponenten gerendert werden – mithilfe von `definePageLayout` und `definePageLayoutTab`.
icon: table-columns
---
Ein **Seitenlayout** steuert, wie die Detailseite eines Datensatzes angeordnet ist: welche Tabs angezeigt werden und welche Widgets sie enthalten. Verwenden Sie `definePageLayout()`, um ein Layout für ein Objekt zu deklarieren, das Sie besitzen, oder `definePageLayoutTab()`, um einen einzelnen Tab zu einem Layout hinzuzufügen, das bereits existiert (Ihr eigenes oder ein standardmäßiges Twenty-Layout).
| Anwendungsfall | Entität |
| ----------------------------------------------------------------------------------------------- | --------------------- |
| Definieren Sie das gesamte Layout für die Datensatzseite eines Objekts, das Sie besitzen | `definePageLayout` |
| Fügen Sie einem vorhandenen Layout einen Tab hinzu (Ihr eigenes Objekt oder ein Standardlayout) | `definePageLayoutTab` |
## definePageLayout
Verwenden Sie dies, wenn Sie die gesamte Detailseite besitzen – typischerweise für ein benutzerdefiniertes Objekt, das Sie selbst definiert haben.
```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,
},
},
],
},
],
});
```
### Hauptpunkte
* `type` ist eines von `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` oder `'STANDALONE_PAGE'`. Verwenden Sie `'RECORD_PAGE'`, um die Detailansicht eines bestimmten Objekts anzupassen.
* `objectUniversalIdentifier` gibt an, auf welches Objekt dieses Layout angewendet wird.
* Jeder `tab` definiert einen Abschnitt der Seite mit `title`, `position` und `layoutMode`: `VERTICAL_LIST` für Datensatzseiten und eigenständige Seiten, `GRID` für Dashboards und `CANVAS` für ein einzelnes Widget, das den Ansichtsbereich des Tabs ausfüllen soll. Ein `VERTICAL_LIST`-Tab ordnet Widgets vertikal an. Integrierte Widgets, die ihr eigenes Scrollen verwalten, wie etwa Timelines, Dateien, Notizen, Aufgaben und Workflows, füllen einen Ansichtsbereich aus; Felder, Front-Komponenten, Diagramme und andere Widgets mit `fit-content` werden mit ihrer Inhalts- oder konfigurierten Höhe dargestellt. Ein `GRID`-Tab ordnet seine Widgets immer als Karten in einem Raster mit 12 Spalten an. Ein `CANVAS`-Widget hat keine explizite Position; wenn ein Canvas-Tab mehrere Widgets enthält, werden sie in ihrer Inhaltshöhe gerendert, anstatt den Viewport auszufüllen.
* Setze `layoutMode` explizit. Wenn du es weglässt, erhältst du `VERTICAL_LIST` auf einer `STANDALONE_PAGE` und `GRID` überall sonst, was auf einer Datensatzseite selten gewünscht ist.
* Jedes `widget` innerhalb eines Tabs kann eine [Frontend-Komponente](/l/de/developers/extend/apps/layout/front-components), eine Relationenliste oder andere eingebaute Widget-Typen rendern.
* Ein `FRONT_COMPONENT`-Widget kann `headerCommandMenuItemUniversalIdentifiers` auf ein geordnetes Array universeller Kennungen von Befehlsmenüelementen aus derselben App festlegen. Diese Aktionen werden als Symbolschaltflächen im Kopf der Widget-Karte angezeigt und behalten ihre Verfügbarkeits- und Berechtigungsprüfungen auf Befehlsebene bei. Kennungen müssen eindeutig sein und bei installierter App aufgelöst werden können.
* `position` auf Tabs steuert deren Reihenfolge. Verwenden Sie höhere Werte (z. B. 50), um benutzerdefinierte Tabs hinter den integrierten zu platzieren.
### Feld-Widgets
Ein `FIELD`-Widget rendert ein Feld des Datensatzes. Bei Relationsfeldern kann es auch eine Liste verwandter Datensätze einbetten:
```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` nimmt den universellen Bezeichner eines Feldes am Objekt des Layouts.
* `fieldDisplayMode` ist eines von `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` oder `'TABLE'`. `TABLE` bettet eine Ansicht ein, die die Datensätze eines 1:n-Relationsfeldes auflistet.
* `nestedRelationFieldMetadataId` ist optional und nimmt den universellen Bezeichner eines Eins-zu-viele-Beziehungsfelds auf dem Beziehungszielobjekt entgegen, um Datensätze aufzulisten, die zwei Beziehungssprünge entfernt sind (z. B. eine Company-Seite, die die Opportunities der Personen des Unternehmens auflistet, oder eine Person-Seite, die die Opportunities des Unternehmens der Person auflistet). Der erste Sprung kann ein Eins-zu-viele- oder ein Viele-zu-eins-Beziehungsfeld sein, der zweite muss Eins-zu-viele sein (Junction-Beziehungen werden nicht unterstützt), und es erfordert `fieldDisplayMode: 'TABLE'` — die Kombination mit jedem anderen Anzeigemodus ist ein Validierungsfehler, da ein verschachteltes Widget immer als eingebettete Ansicht gerendert wird.
## definePageLayoutTab
Verwenden Sie dies, wenn Sie nur einen Tab zu einem vorhandenen Layout **hinzufügen** möchten – zum Beispiel einen Analytics-Tab auf der standardmäßigen Company-Seite oder einen KI-Zusammenfassungs-Tab, der an das Layout Ihres eigenen Objekts angehängt ist.
```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,
},
},
],
});
```
### Hauptpunkte
* `pageLayoutUniversalIdentifier` ist **erforderlich** und muss auf ein Seitenlayout verweisen, das zum Installationszeitpunkt bereits existiert – entweder ein standardmäßiges Twenty-Layout oder eines, das von Ihrer eigenen App definiert wurde. App-übergreifende Verweise auf Layouts, die einer anderen installierten App gehören, werden derzeit nicht unterstützt. Wenn das übergeordnete Layout fehlt, schlägt die Installation mit einem eindeutigen Validierungsfehler fehl.
* Für Standard-Twenty-Layouts importieren Sie die Bezeichner aus `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
// …
```
Jeder Layout-Eintrag stellt außerdem seine `tabs` und deren `widgets` zur Verfügung, sodass Sie auf jede Ebene verweisen können:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Eine kurze Alias-Variable `STANDARD_PAGE_LAYOUT` ist ebenfalls verfügbar:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` sind ausschließlich auf diesen Tab beschränkt – sie verweisen auf [Frontend-Komponenten](/l/de/developers/extend/apps/layout/front-components), Ansichten usw., genau wie Widgets, die inline in `definePageLayout` definiert sind.
* `position` steuert die Reihenfolge im Zielseitenlayout relativ zu den vorhandenen Registerkarten. Wählen Sie einen Wert, der Ihre Registerkarte relativ zu integrierten Registerkarten an die gewünschte Position bringt.
* Verwenden Sie dies anstelle von `definePageLayout`, wenn Sie einem vorhandenen Layout nur etwas hinzufügen möchten. Verwenden Sie `definePageLayout`, wenn Sie das gesamte Layout besitzen.

View File

@@ -0,0 +1,113 @@
---
title: Ansichten
description: Stellen Sie vorkonfigurierte gespeicherte Ansichten bereit – Spaltenreihenfolge, Filter, Gruppen – für Objekte in Ihrer App.
icon: list
---
Eine **Ansicht** ist eine gespeicherte Konfiguration dafür, wie Datensätze eines Objekts angezeigt werden: welche Felder erscheinen, in welcher Reihenfolge, ob sie sichtbar sind und welche Filter oder Gruppen angewendet werden. Verwenden Sie `defineView()`, um vorkonfigurierte Ansichten mit Ihrer App auszuliefern.
Jedes Objekt erhält automatisch seine Hauptlistenansicht (die "Alle ..."-Tabelle, die ein `OBJECT`-Navigationselement öffnet), die vom Server bereitgestellt wird – einschließlich der Objekte, die Ihre App erstellt, mit einer Spalte pro Feld, das Ihre App definiert. Ansichten, die Sie mit `defineView()` definieren, sind stets **zusätzliche** Ansichten neben der Standardansicht.
```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,
},
],
});
```
## Hauptpunkte
* `objectUniversalIdentifier` gibt an, auf welches Objekt diese Ansicht angewendet wird. Es kann sich um ein von Ihnen definiertes benutzerdefiniertes Objekt oder ein Standardobjekt von Twenty handeln.
* Die Hauptlistenansicht des Objekts gehört dem Server: `key` ist veraltet und wird ignoriert, daher kann eine Manifest-Ansicht sie niemals beanspruchen. Liefern Sie ein `VIEW`-Navigationselement aus, wenn Ihre Ansicht nur einen Klick entfernt in der Seitenleiste sein soll.
* `fields` steuert, welche Spalten erscheinen und in welcher Reihenfolge. Jedes Feld referenziert einen `fieldMetadataUniversalIdentifier`. Informationen zum Verweisen auf ein automatisch erstelltes Systemfeld wie `createdAt` finden Sie unter [Systemmetadaten ansteuern](/l/de/developers/extend/apps/data/system-fields).
* Für erweiterte Konfigurationen können Sie außerdem `filters`, `filterGroups`, `sorts`, `groups` und `fieldGroups` deklarieren.
* `position` steuert die Reihenfolge, wenn mehrere Ansichten für dasselbe Objekt existieren.
## Optionale Eigenschaften
| Eigenschaft | Werte | Beschreibung |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `ViewType.TABLE` (Standard), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Wie Datensätze angeordnet werden. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` und `CALENDAR_WIDGET` existieren ebenfalls, werden aber intern von Page-Layout-Widgets verwendet.) |
| `visibility` | `ViewVisibility.WORKSPACE` (Standard), `ViewVisibility.UNLISTED` | Ob die Ansicht für den gesamten Workspace aufgelistet oder in Auswahlelementen verborgen ist. |
| `openRecordIn` | veraltet | Nicht mehr ausgelesen: Wo Datensätze geöffnet werden, ist jetzt eine Eigenschaft des [Objekts](/l/de/developers/extend/apps/data/objects) (`openRecordIn` bei `defineObject()`) und fällt auf die jeweilige Präferenz des einzelnen Mitglieds zurück. |
| `sortierungen` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Standard-Sortierreihenfolge. |
| `isCompact` | `boolean` | Kompakte Zeilenanzeige. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Datensätze (z. B. Kanban-Spalten) nach einem Feld gruppieren. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Aggregationen und Größen von Kanban-Spalten. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Kalenderansichten: Layout, das Datumsfeld, das die Position der Datensätze bestimmt, und ein optionales Enddatumsfeld. |
Alle oben genannten Enums werden aus `twenty-sdk/define` exportiert.
## Filter
Eine Ansicht kann mit vorab angewendeten Filtern ausgeliefert werden. Jeder Filter hat drei Koordinaten: das **Feld**, das gefiltert wird, der **Operand** (wie verglichen wird) und der **Wert** (womit verglichen wird). Alle drei müssen übereinstimmen — die Verwendung eines Operanden, der nicht auf einen Feldtyp anwendbar ist, wird bei der Synchronisierung zurückgewiesen.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Unterstützte Operanden pro Feldtyp
| Feldtyp | Unterstützte Operanden |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `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` |
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden – `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
### Wertstruktur je Operand
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber die erwartete Struktur hängt vom Operanden ab:
| Operandenfamilie | Wertstruktur | Beispiel |
| -------------------------------------------------------- | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` bei `SELECT` | Array von Optionsschlüsseln (Strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei `MULTI_SELECT` | Array von Optionsschlüsseln (Strings) | `['TAG_A']` |
| `IS`, `IS_NOT` bei `RELATION` | Array von Datensatz-IDs (UUIDs) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei textähnlichen Feldern | Zeichenkette | `'acme'` |
| `IS`, `IS_NOT` bei `NUMBER` | String (der Wert) | `'5'` |
| `IS` bei `RATING` / `UUID` | String (der Wert) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (der Grenzwert) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` bei `DATE` / `DATE_TIME` | ISO-8601-String | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | leerer String | `''` |
| `IS` bei `BOOLEAN` | `'true'` oder `'false'` | `'true'` |
## Wie Ansichten in der UI angezeigt werden
Eine Ansicht für sich ist aus der Seitenleiste nicht erreichbar. Damit sie dort erscheint, verknüpfen Sie sie mit einem [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items) des Typs `VIEW`, der auf den `universalIdentifier` der Ansicht zeigt. Das ist das kanonische Muster: Für jedes benutzerdefinierte Objekt wird bei der Erstellung automatisch eine Standard-Indexansicht generiert, sodass du nur den Sidebar-Eintrag ausliefern musst, der sie öffnet.