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: コマンドメニュー項目
description: defineCommandMenuItem を使用して、フロントコンポーネントをクイックアクションやコマンドメニュー (Cmd+K) のエントリとして表示します。
icon: terminal
---
**コマンドメニュー項目**は、ユーザーと[フロントコンポーネント](/l/ja/developers/extend/apps/layout/front-components)をつなぐブリッジです。 これは Twenty のコマンドメニュー (Cmd+K) にコンポーネントを登録し、必要に応じて、ページ右上のピン留めされたクイックアクションボタンとしても登録します。
```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',
});
```
## 設定フィールド
| フィールド | 必須 | 説明 |
| --------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | はい | コマンドの安定した一意の ID |
| `label` | はい | コマンドメニュー(Cmd+K)に表示されるフルラベル |
| `frontComponentUniversalIdentifier` | はい | このコマンドが開くフロントコンポーネントの `universalIdentifier` |
| `shortLabel` | いいえ | ピン留めされたクイックアクションボタンに表示される短いラベル |
| `icon` | いいえ | **非推奨** — アプリケーションアイコンが優先されるため無視されます。設定されている場合は、ビルド時に警告が出力されます。 |
| `isPinned` | いいえ | `true` の場合、ページ右上にクイックアクションボタンとして表示します |
| `availabilityType` | いいえ | コマンドの表示場所を制御します:`'GLOBAL'`(常に利用可能)、`'GLOBAL_OBJECT_CONTEXT'`(オブジェクトコンテキストを持つページ上のみ ― インデックスページおよびレコードページ)、`'RECORD_SELECTION'`(レコード選択時のみ)、または`'FALLBACK'`(他のコマンドが一致しないときに表示) |
| `availabilityObjectUniversalIdentifier` | いいえ | コマンドを特定のオブジェクトタイプのページに制限します(例:Company レコードのみ) |
| `conditionalAvailabilityExpression` | いいえ | 表示可否を動的に制御するブール式(下記参照) |
## ヘッドレスコマンド
[ヘッドレスフロントコンポーネント](/l/ja/developers/extend/apps/layout/front-components#headless-vs-non-headless)とペアになったコマンドメニュー項目は、ワンクリックアクション(コードの実行、ナビゲーション、確認と実行)を提供するための一般的な方法です。 Front Components のページでは、アクション実行後にアンマウントするパターンを処理する [SDK Command components](/l/ja/developers/extend/apps/layout/front-components#sdk-command-components)(`Command`、`CommandLink`、`CommandModal`、`CommandOpenSidePanelPage`)について説明しています。
一般的なフロー:ヘッドレスコンポーネントが `<Command execute={...} />` をレンダーし([完全なサンプル](/l/ja/developers/extend/apps/layout/front-components#sdk-command-components)を参照)、コマンドメニュー項目がそれを指すようにします:
```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',
});
```
## 条件付き可用性式
`conditionalAvailabilityExpression` フィールドにより、現在のページコンテキストに基づいてコマンドの表示タイミングを制御できます。 式を構築するために、`twenty-sdk` から型付きの変数と演算子をインポートします:
```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` は、すでに空ではない選択状態を意味します。特定の件数を示す場合にのみ `numberOfSelectedRecords` を使用してください(例: `>= 2`)。
</Note>
### コンテキスト変数
これらは現在のページの状態を表します:
| 変数 | タイプ | 説明 |
| ------------------------------ | --------- | ----------------------------------------------- |
| `pageType` | `string` | 現在のページタイプ(例:'RecordIndexPage'、'RecordShowPage') |
| `isInSidePanel` | `boolean` | コンポーネントがサイドパネル内でレンダリングされているかどうか |
| `numberOfSelectedRecords` | `number` | 現在選択されているレコード数 |
| `isSelectAll` | `boolean` | 「すべて選択」が有効かどうか |
| `selectedRecords` | `array` | 選択されたレコードオブジェクト |
| `favoriteRecordIds` | `array` | お気に入り登録されたレコードの ID |
| `objectPermissions` | `object` | 現在のオブジェクトタイプの権限 |
| `targetObjectReadPermissions` | `object` | 対象オブジェクトの読み取り権限 |
| `targetObjectWritePermissions` | `object` | 対象オブジェクトの書き込み権限 |
| `featureFlags` | `object` | 有効な機能フラグ |
| `objectMetadataItem` | `object` | 現在のオブジェクトタイプのメタデータ |
| `hasAnySoftDeleteFilterOnView` | `boolean` | 現在のビューにソフトデリートフィルターがあるかどうか |
### 演算子
変数を組み合わせてブール式を作成します:
| 演算子 | 説明 |
| ----------------------------------- | ----------------------------------------- |
| `isDefined(value)` | 値が null/undefined でない場合は `true` |
| `isNonEmptyString(value)` | 値が空でない文字列の場合は `true` |
| `includes(array, value)` | 配列に値が含まれている場合は `true` |
| `includesEvery(array, prop, value)` | すべての要素のプロパティに値が含まれている場合は `true` |
| `every(array, prop)` | すべての要素でそのプロパティが truthy の場合は `true` |
| `everyDefined(array, prop)` | すべての要素でそのプロパティが定義されている場合は `true` |
| `everyEquals(array, prop, value)` | すべての要素でそのプロパティが値に等しい場合は `true` |
| `some(array, prop)` | 少なくとも 1 つの要素でそのプロパティが truthy の場合は `true` |
| `someDefined(array, prop)` | 少なくとも 1 つの要素でそのプロパティが定義されている場合は `true` |
| `someEquals(array, prop, value)` | 少なくとも 1 つの要素でそのプロパティが値に等しい場合は `true` |
| `someNonEmptyString(array, prop)` | 少なくとも 1 つの要素でそのプロパティが空でない文字列である場合は `true` |
| `none(array, prop)` | すべての要素でそのプロパティが falsy の場合は `true` |
| `noneDefined(array, prop)` | すべての要素でそのプロパティが未定義の場合は `true` |
| `noneEquals(array, prop, value)` | どの要素でもそのプロパティが値に等しくない場合は `true` |

View File

@@ -0,0 +1,849 @@
---
title: フロントコンポーネント
description: Twenty の UI 内でレンダリングされる、サンドボックスで分離された React コンポーネントを構築します。
icon: window-maximize
---
フロントコンポーネントは、Twenty の UI 内で直接レンダリングされる React コンポーネントです。 フロントコンポーネントは Remote DOM を使用する**分離された Web Worker**内で実行されます。コードはサンドボックス化され、不透明なオリジンの iframe 内で動作しますが、その UI はその iframe 内に制限されるのではなく、ページ内でネイティブにレンダリングされます。
<Warning>
Front components は現在も積極的に開発が進められています。 あなたのコードは実際のブラウザページではなく不完全な DOM に対して実行されるため、高度な使い方では、しばしば何の表示もなく失敗することがあります。 [現在の制限](#current-limitations) を参照してください。
</Warning>
## フロントコンポーネントを使用できる場所
フロントコンポーネントは、Twenty 内の3つの場所でレンダリングできます:
* **サイドパネル** — ヘッドレスでないフロントコンポーネントは、右側のサイドパネルで開きます。 フロントコンポーネントがコマンドメニューからトリガーされた場合のデフォルトの動作です。
* **ウィジェット(ダッシュボードとレコードページ)** — フロントコンポーネントは、[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)内にウィジェットとして埋め込めます。 ダッシュボードやレコードページのレイアウトを設定する際、ユーザーはフロントコンポーネントのウィジェットを追加できます。
* **App settings** — [`defineSettingsFrontComponent()`](#custom-settings-component) で定義されたフロントコンポーネントは、アプリの **Settings** タブ内のセクションとして、デフォルトの変数設定 UI の代わりにレンダリングされます。
フロントコンポーネント単体では UI から直接アクセスできないため、それを*表示*する必要があります。 それを行う方法は次の3つです。
* **[コマンドメニュー項目](/l/ja/developers/extend/apps/layout/command-menu-items)とペアにする** — コマンドメニュー(Cmd+K)に登録し、必要に応じてピン留めされたクイックアクションとして登録します。
* **[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)内のウィジェットとして埋め込む** — レコードの詳細ページまたはダッシュボード上に配置します。
* **[`defineSettingsFrontComponent()`](#custom-settings-component) で定義する** — アプリの **Settings** タブ内のセクションとして、デフォルトの変数設定 UI の代わりにレンダリングされます。
## 基本的な例
フロントコンポーネントの動作を手早く確認するには、[`defineCommandMenuItem`](/l/ja/developers/extend/apps/layout/command-menu-items)とペアにして、ページ右上隅にクイックアクションボタンとして表示させるのが最も簡単です。
```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',
});
```
`yarn twenty dev` で同期するか(または 1 回限りで `yarn twenty apply` を実行すると)、ページ右上にクイックアクションが表示されます:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="右上のクイックアクションボタン" />
</div>
クリックすると、コンポーネントがインラインでレンダリングされます。
## 設定フィールド
| フィールド | 必須 | 説明 |
| --------------------- | --- | ----------------------------------------- |
| `universalIdentifier` | はい | このコンポーネントの安定した一意の ID |
| `component` | はい | React コンポーネント関数 |
| `name` | いいえ | 表示名 |
| `description` | いいえ | コンポーネントの機能の説明 |
| `isHeadless` | いいえ | コンポーネントに可視の UI がない場合は `true` を設定します(下記参照) |
## フロントコンポーネントをページに配置する
コマンド以外にも、**ページレイアウト**でウィジェットとして追加することで、フロントコンポーネントをレコードページに直接埋め込めます。 詳しくは[ページレイアウト](/l/ja/developers/extend/apps/layout/page-layouts)を参照してください。
## カスタム設定コンポーネント
アプリの **Settings** タブ内の自動生成された変数設定 UI を独自のコンポーネントに置き換えるには、`defineFrontComponent` ではなく `defineSettingsFrontComponent` で定義します。 このコンポーネントは、同じ[configuration fields](#configuration-fields)(ただし、設定コンポーネントは常に可視の UI をレンダーするため、受け付けられない `isHeadless` を除く)を受け取り、さらにこのコンポーネントをアプリの設定 UI としてマークします。
このコンポーネントは、Settings タブ全体を置き換えるのではなく、そのタブの内部のセクションとしてレンダリングされます。 Twenty のシステム管理セクション(自動アップグレード、App URL、接続)は常にその上にレンダーされ、アプリ側で上書きすることはできません。
```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,
});
```
1 つのアプリにつき許可される settings front コンポーネントは 1 つだけであり、2 つ以上を宣言するとビルドは失敗します。 存在する場合、アプリの **Settings** タブはデフォルトの変数設定 UI の代わりにこのコンポーネントをレンダーします。
## ヘッドレスと非ヘッドレス
フロントコンポーネントには、`isHeadless` オプションで制御される2つのレンダリングモードがあります:
**非ヘッドレス(デフォルト)** — コンポーネントは可視のUIをレンダリングします。 コマンドメニューからトリガーされた場合、サイドパネルで開きます。 `isHeadless` が `false` または省略された場合のデフォルトの動作です。
**ヘッドレス (`isHeadless: true`)** — コンポーネントはバックグラウンドで不可視のままマウントされます。 サイドパネルは開きません。 ヘッドレスコンポーネントは、ロジックを実行して自動的にアンマウントするアクション向けに設計されています。例えば、非同期タスクの実行、ページへのナビゲーション、確認モーダルの表示などです。 以下で説明する SDK の Command コンポーネントと自然に組み合わせて使用できます。
```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,
});
```
このコンポーネントが `null` を返すため、Twenty はそのためのコンテナのレンダリングをスキップします—レイアウトに空白は発生しません。 コンポーネントは引き続き、すべてのフックとホスト通信 API にアクセスできます。
## SDK の Command コンポーネント
`twenty-sdk` パッケージは、ヘッドレスのフロントコンポーネント向けに設計された4つの Command ヘルパーコンポーネントを提供します。 各コンポーネントは、マウント時にアクションを実行し、エラーをスナックバー通知で処理し、完了時にフロントコンポーネントを自動的にアンマウントします。
`twenty-sdk/front-component` からインポートします:
* **`Command`** — `execute` プロップ経由で非同期コールバックを実行します。
* **`CommandLink`** — アプリのパスにナビゲートします。 Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — 確認モーダルを開きます。 ユーザーが確認すると、`execute` コールバックを実行します。 Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — サイドパネルページを開きます。 Props は `page` に依存します。たとえば、`ViewRecord` は `recordId` と `objectNameSingular`(さらに任意で、特定のタブでレコードを開くための `tab` id)を受け取り、他のページは `pageTitle` と `pageIcon` を受け取ります。
`Command` を使用してコマンドメニューからアクションを実行するヘッドレスのフロントコンポーネントの完全な例です:
```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',
});
```
さらに、実行前に確認を求めるために `CommandModal` を使用する例です:
```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,
});
```
そして、`CommandOpenSidePanelPage` を使用して、現在のレコードを特定のタブのサイドパネルで開く例です。 `tab` はページレイアウトのタブ id です(デフォルトのレイアウトでは `company-tab-emails` や `company-tab-timeline` のような id を使用し、カスタムレイアウトではタブ自身の id を使用します)。 その id がレコードのレイアウト内に存在しない場合は、代わりにデフォルトのタブが開きます。
```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,
});
```
## ロジック関数の呼び出し
フロントコンポーネントは不透明なオリジンの iframe 内にサンドボックス化された Web Worker 内でブラウザーサイドで実行され、一方で[ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)はサーバーサイドで実行されます。 両者の間にプロセス内での直接呼び出しはありません。その代わり、フロントコンポーネントは HTTP 経由でロジック関数にアクセスします。
`httpRouteTriggerSettings` で宣言されたロジック関数は、そのルートパスで HTTP 経由でアクセスできます。 `RestApiClient` は、`/s/` で始まるパスをアプリのルートとして扱い、それらをあなたの関数が提供されている URL に解決し、`TWENTY_APP_ACCESS_TOKEN` で認証します。
> **Twenty Cloud では、HTTP トリガーのロジック関数はワークスペースごとの専用ドメインで提供されます**。`https://\<your-workspace-subdomain>.withtwenty.com\<path>` で提供されます。 外部から呼び出す場合は、関数の **HTTP trigger** 設定、もしくはアプリケーションの **Settings** タブから、正確な URL をコピーしてください。
ヘッドレスフロントコンポーネントは、`Command` コンポーネント経由でマウント時に呼び出しを実行し、その後自動的にアンマウントできます。
```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,
});
```
`RestApiClient` に渡されるパスは、ロジック関数の `httpRouteTriggerSettings.path` に `/s` を付加したものです。 `isAuthRequired: true` のままにしておいてください。コンポーネント用に Twenty が発行する `TWENTY_APP_ACCESS_TOKEN` によってリクエストが認証されます。
```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` は自動的に挿入されます。詳しくは [Application variables](#application-variables) を参照してください。 秘匿アプリケーション変数はフロントコンポーネントに公開されることがないため、API キーやその他の機密性の高いロジックはフロントコンポーネントではなく、ロジック関数側に保持してください。
</Note>
### Twenty REST API の呼び出し
アプリの HTTP ルートを呼び出したり、フロントコンポーネントから Twenty のレコードを読み書きしたりするには、`twenty-client-sdk/rest` の `RestApiClient` を使用します。 これは、`/s/...` のパスをワークスペースの関数のベース URL に送り、`/rest/...` を含むそれ以外のすべてのパスを `TWENTY_API_URL` に送信します。
常にページを見ている人として動作します。 `runAs: 'application'` はロジック関数専用のオプションです。コンポーネントにはアプリケーション自身のトークンが渡されないため、ここでそれを要求するとエラーになります。 アプリ自身のアクセスが必要な処理はロジック関数内に置き、代わりにその関数を呼び出してください。
| メソッド | 説明 |
| --------------------------------- | ------------------------------------ |
| `get(path, options?)` | `GET` リクエストを送信します |
| `post(path, body?, options?)` | `POST` リクエストを送信します |
| `put(path, body?, options?)` | `PUT` リクエストを送信します |
| `patch(path, body?, options?)` | `PATCH` リクエストを送信します |
| `delete(path, options?)` | `DELETE` リクエストを送信します |
| `request(method, path, options?)` | 任意の HTTP メソッドによる汎用的なリクエスト |
| `resolveUrl(path, options?)` | リクエストを送信せずに、パスを完全な URL に解決します(リンク向け) |
`options` には、`headers`、`query`(クエリ文字列パラメーターのレコード。null 相当の値はスキップされます)、および `signal` 経由の `AbortSignal` を指定できます。 `FormData` ではないオブジェクト `body` は、自動的に JSON シリアル化されます。 `401` が発生した場合、クライアントはホスト経由で一度だけアクセス トークンを更新し、そのリクエストを再試行します。
ベース URL とトークンは、デフォルトで環境から解決されます。 必要に応じてコンストラクターに上書き設定を渡します — たとえばテスト時などです:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
失敗したリクエストは `RestApiClientError` をスローし、`status`、`statusText`、`url`、および解析済みの `body` を公開します:
```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);
}
}
```
## ランタイムコンテキストへのアクセス
コンポーネント内で、SDK のフックを使用して現在のユーザー、レコード、コンポーネントインスタンスにアクセスします:
```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,
});
```
利用可能なフック:
| フック | 戻り値 | 説明 |
| --------------------------------------------- | ---------------------- | ------------------------------------------------ |
| `useUserId()` | `string` または `null` | 現在のユーザーの ID |
| `useSelectedRecordIds()` | `string[]` | 選択されたレコードIDの配列(未選択の場合は空配列) |
| `useRecordId()` | `string` または `null` | **非推奨。** 代わりに `useSelectedRecordIds()` を使用してください |
| `useFrontComponentId()` | `string` | このコンポーネントインスタンスの ID |
| `useTimelineActivityId()` | `string` または `null` | カスタムタイムライン行をレンダリングする際の現在のタイムラインアクティビティ ID |
| `useColorScheme()` | `'light'` または `'dark'` | ホスト UI のアクティブなカラースキーム(`System` はすでに解決済み) |
| `useFrontComponentExecutionContext(selector)` | 項目により異なる | セレクター関数で実行コンテキスト全体にアクセス |
## アプリケーション変数
`isSecret: false` が設定された [`defineApplication()`](/l/ja/developers/extend/apps/config/application) 内で定義されたアプリケーション変数は、`getApplicationVariable` ユーティリティを通じてフロントコンポーネント内で利用できます。
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
シークレット変数(`isSecret: true`)はフロントコンポーネントには公開**されません**。 それらは、サーバーサイドで実行される[ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions)でのみ利用できます。 これにより、API キーなどの機密値がブラウザーに送信されるのを防ぎます。
</Warning>
`getApplicationVariable` は、変数に宣言されている `type` に関係なく、常に **string**(または `undefined`)を返します。 文字列は型に応じて一貫した方法でシリアライズされます(boolean は `"true"` / `"false"`、number は 10 進数の文字列、配列 / オブジェクトは JSON)。これはロジック関数の `process.env` で使用されているのと同じ形式です。各自でパースしてください(`Number(...)`、`JSON.parse(...)`、`=== 'true'` など)。 [Variable types](/l/ja/developers/extend/apps/config/application#variable-types) を参照してください。
次のシステム変数は、常に `process.env` 経由で利用できます。
| 変数 | 説明 |
| ------------------------- | -------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | Twenty コア API のベース URL |
| `TWENTY_APP_ACCESS_TOKEN` | サインインしている人のロールとアプリのロールの共通部分にスコープ設定された短期間有効なトークン。そのため、コンポーネントが閲覧者以上の権限を持つことはありません |
### `TWENTY_FUNCTIONS_URL`
Twenty はまた、`TWENTY_FUNCTIONS_URL` をフロントコンポーネントとロジック関数に挿入します。これは、アプリの HTTP トリガーのロジック関数が提供されるベース URL です。
この変数が存在するのは、その URL が必ずしも Twenty サーバー自体とは限らないためです。 Twenty Cloud では、アプリのルートはワークスペースごとの専用ドメイン(`https://\<your-workspace-subdomain>.withtwenty.com`、または設定されている場合はアプリケーションのプライマリ公開ドメイン)で提供されます。これにより、アプリで作成されたレスポンスが Twenty アプリのオリジンではなく分離されたオリジン上で実行されます。 セルフホストおよびローカルインスタンスでは、アプリのルートはサーバー自体の `/s` プレフィックスの下で提供され、この変数がまったく設定されない場合もあります。 ベース URL はワークスペースやインスタンスごとに異なるため、コードでハードコードすることはできません。サーバーが実行時に正しい値を挿入します。
この変数を直接読む必要があることはほとんどありません。 `/s/` プレフィックス付きのパスで `RestApiClient` を通じてルートを呼び出すと、クライアントが URL を解決します。`/s` プレフィックスを取り除き、`TWENTY_FUNCTIONS_URL` をターゲットにし、この変数が設定されていない場合は `\<TWENTY_API_URL>/s` をフォールバックとして使用します。 リクエストを送信せずに絶対 URL を取得するには、`resolveUrl('/s/\<path>')` を使用します(リンクなどの用途)。 URL を手作業で組み立てる場合にのみ、この変数を直接読み取ってください。
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## ホスト通信 API
フロントコンポーネントは、`twenty-sdk` の関数を使用してナビゲーション、モーダル、通知をトリガーできます:
| 機能 | 説明 |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | アプリ内のページに移動 |
| `openSidePanelPage(params)` | サイドパネルを開く |
| `closeSidePanel()` | サイドパネルを閉じる |
| `openCommandConfirmationModal(params)` | 確認ダイアログを表示 |
| `enqueueSnackbar(params)` | トースト通知を表示 |
| `unmountFrontComponent()` | コンポーネントをアンマウント |
| `updateProgress(progress)` | 進行状況インジケーターを更新 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | `Blob` を FILES フィールドにアップロードします。返り値は `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` または `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` です。 |
アクション完了後にホスト API を使用してスナックバーを表示し、サイドパネルを閉じる例です:
```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,
});
```
### ストレージ
`localStorage` と `sessionStorage` は、通常のページと同様に、標準的な同期 API として動作します。 キーはアプリのインストールとサインインしているユーザーにスコープされており、他のアプリがそれらを読み取ることはできません。また、同じブラウザに別のユーザーがサインインした場合は、空のストアから開始します。 `localStorage` に書き込まれた値は、リロードをまたいでもデバイス上に保持されます。一方、`sessionStorage` はブラウザセッションの間だけ有効です。
```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 はアプリに代わって値を保存するため、書き込みはローカルに即座に反映され、その後バックグラウンドで保存されます。 読み取り処理がホストで待機することはありません。 何も同期されません。値は別のブラウザやマシンには引き継がれないため、デバイス変更後も保持する必要があるものには、ロジック関数の[key-value store](/l/ja/developers/extend/apps/logic/key-value-store)を使用してください。
書き込みには上限があり、すべての制限はバイト数ではなく文字数でカウントされます。キーは最大 512 文字、1 つの値は最大 262,144 文字、各ストレージはアプリおよびユーザーごとに最大 1,048,576 文字です。 制限を超える書き込みは、ブラウザ API と同様に `QuotaExceededError` をスローします。
### 複数のレコードを扱う
複数の選択されたレコードを処理するには `useSelectedRecordIds()` を使用してください。 これは一括操作に役立ちます:
```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,
});
```
レコードの選択に制限された[コマンドメニューアイテム](/l/ja/developers/extend/apps/layout/command-menu-items)として表示します:
```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',
});
```
## 公開アセット
フロントコンポーネントは、`getPublicAssetUrl` を使用してアプリの `public/` ディレクトリ内のファイルにアクセスできます:
```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,
});
```
詳細は[公開アセットのセクション](/l/ja/developers/extend/apps/config/public-assets)を参照してください。
## フロントコンポーネント間で依存関係を共有することについて
デフォルトでは、各フロントコンポーネントはインポートするライブラリのコピーをそれぞれ同梱するため、5つのコンポーネントを持つアプリでは、React が5回配信されます。 共有する依存関係をアプリの `package.json` に宣言すると、それらのライブラリを一度だけビルドし、アプリ内のすべてのコンポーネントが単一のキャッシュ済みファイルから読み込むようにできます:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
その後、各コンポーネントはこれまでどおり依存関係をインポートするだけで済み、コンポーネントコード側で変更する必要はありません:
```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,
});
```
知っておくべきこと:
* **アプリごとに1つの共有依存関係バンドル。** バンドルはアプリ自身の依存関係からビルドされるため、配信するバージョンを完全にコントロールできます。
* **インポートする正確な指定子を列挙してください。** `twenty-ui/input` と `twenty-ui/display` は 2 つのエントリです。パッケージ名だけでは、そのサブパスは対象になりません。 `react` を列挙すると、自動的に `react/jsx-runtime` も対象になります。
* **`react` と一緒に `react-dom/client` を共有してください。** すべてのコンポーネントは `createRoot` を通じてレンダーされるため、これを含めない場合、各コンポーネントは引き続き React DOM を同梱することになります。
* **バンドルはキャッシュされます。** バンドルは長期間有効な不変キャッシュ付きのコンテンツハッシュ URL で提供されるため、一度ダウンロードされると、その依存関係のいずれかが変更されるまでアプリのすべてのコンポーネントで再利用されます。
* **共有パッケージを一切インポートしないコンポーネントは、バンドルをダウンロードすることはありません。**
## スタイリング
フロントコンポーネントは複数のスタイリング手法をサポートしています。 次のものを使用できます:
* **インラインスタイル** — `style={{ color: 'red' }}`
* **Twenty UI コンポーネント** — Twenty 独自のコンポーネントライブラリです。以下の [Using Twenty UI components](#using-twenty-ui-components) を参照してください
* **Emotion** — `@emotion/react` による CSS-in-JS
* **styled-components** — `styled.div` パターン
* **Tailwind CSS** — ユーティリティクラス
* React と互換性のある**任意の CSS-in-JS ライブラリ**
## Twenty UI コンポーネントの使用
Twenty はコンポーネントライブラリを [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1) パッケージとして提供しています。 フロントエンドコンポーネントでは、ボタン、タグ、ステータスピル、チップ、アバター、アイコン、タイポグラフィ、そしてワークスペースのライト/ダークテーマに自動で合わせてくれるテーマトークンなどに利用できます。
### インストール
Twenty インスタンスに同梱されているバージョンに固定して、そのパッケージをアプリに追加します。
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` はビルド時にフロントエンドコンポーネントへバンドルされるため、アプリの依存関係に追加するだけで済み、実行時に設定することは何もありません。
### コンポーネントのインポート
パッケージのルートではなく、対応するサブパスからインポートすることで、使用しているコンポーネントだけがバンドルに含まれるようにします。
| サブパス | エクスポート内容 |
| --------------------------- | ---------------------------------------- |
| `twenty-ui/input` | `Button` とフォーム入力 |
| `twenty-ui/data-display` | `Tag`、`Status`、`Chip`、`Avatar` など |
| `twenty-ui/feedback` | `Callout`、`Banner`、`Info` など |
| `twenty-ui/typography` | `H1Title`、`H2Title`、`H3Title`、`Label` など |
| `twenty-ui/icon` | `Icon*` コンポーネント(例: `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,
});
```
### アイコン
`twenty-ui/icon` から個別のアイコンをインポートします:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
それぞれの名前付きアイコンはツリーシェイクされるため、少数をインポートしてもバンドルサイズへの影響はわずかです。 `IconsProvider`、`useIcons`、`iconsState` の使用は避けてください。これらは Tabler アイコンセット全体(数 MB)を読み込みます。
### テーマ設定とテーマトークン
Twenty UI コンポーネントはワークスペースのライト/ダークテーマに自動的に追従します。レンダラーがホスト上のアクティブなカラースキームを適用し、コンポーネントはそれに基づいて色を決定します。
独自のインラインスタイルでも同じデザイントークンを使うには、`useTheme()` フックを呼び出します。 これにより、アクティブなテーマに紐づいた Twenty のテーマトークン(スペーシング、カラー、角丸、フォント)が返されます。コンポーネント側で `ThemeProvider` をセットアップする必要はありません。
```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>
);
};
```
`useTheme()` はフックなので、コンポーネント本体の中でトークンを読み取り、値が常に現在のテーマを反映するようにできます。 同じトークンマップは `themeCssVariables` 定数としてもエクスポートされていますが、フロントエンドコンポーネントでは `useTheme()` を優先してください。アプリのマニフェストを抽出している間は、`themeCssVariables` を参照するモジュールレベルの定数が未定義になる可能性があります。
アクティブなスキームを明示的に分岐させるには、`twenty-sdk/front-component` の `useColorScheme()` を使って取得します。このフックは `'light'` または `'dark'` を返します。
## 現在の制限
Front components は現在も積極的に開発が進められています。 レンダリング、スタイリング、イベント処理、要素の計測、およびブラウザー ストレージの操作は問題なく動作します。 それらを*越えた*処理(ref に対する DOM メソッドの呼び出し、要素リサイズの監視、ツリーの外へのポータル)については、現在は未実装または不完全であり、そのほとんどは例外も出さずに黙って失敗します。スキャフォールドがフルなブラウザー DOM を前提に型付けされているため、TypeScript エラーも発生しません。
これらのいずれかが原因でブロックされている場合は、[issue を作成](https://github.com/twentyhq/twenty/issues/new/choose)して、優先度を上げてもらってください。
### レイアウトと計測
要素は自分自身を計測できます。ホストがジオメトリをサンドボックス側にミラーリングするため、読み取りはローカルで応答されますが、最大 1 フレーム分までは古い値になる可能性があり、まだ一度も計測されていない要素の最初の読み取りは 0 を返します。 書き込み後は、`requestAnimationFrame` コールバックまたはエフェクト内で再度読み取ってください。
| API | 何が起きるか |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | ミラーからは動作しますが、`scrollTop` / `scrollLeft` を代入しても何も起こりません (no-op) |
| `getClientRects()`, `window.matchMedia()` | 例外を投げます |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | **ブラウザーのビューポート** を報告する点では動作します。ウィジェット自体のサイズは `document.body.clientWidth` / `clientHeight` です |
| `window.getComputedStyle()` | 要素のインラインスタイルのみを返し、ホストで計算されたカスケードは一切返しません |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError`(`typeof` ガードは機能します) |
| `MutationObserver` | `subtree`、`attributeFilter`、旧値、および `takeRecords()` を含めて動作します |
`getBoundingClientRect` による位置決めは現在動作しますが、`ResizeObserver` を介してサイズ変更を監視しているもの(recharts の `ResponsiveContainer`、Floating UI の `autoUpdate`)は依然として動作しません。 レイアウトにはいずれにせよ CSS を優先してください。スタイルシートは実際のページに適用されるので、flexbox、grid、`aspect-ratio`、`clamp()`、`@container` はフレーム遅延なしで通常どおり動作します。
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout`, `queueMicrotask` は、`window.` プレフィックスなしで動作します。 `window.requestAnimationFrame(...)` などを使った場合だけ例外が投げられます。
</Note>
### DOM アクセス
`ref` が返すのは `HTMLElement` ではなくサンドボックス要素です。
| 記述するコード | 何が起きるか | 代わりに使うもの |
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | 例外を投げます | 制御されたコンポーネントを使い、値は `event.target` から読み取ります |
| `element.classList.add(...)` | 例外を投げます(`classList` は `undefined` です) | `className` 文字列を自分で組み立ててください |
| `document.createTreeWalker()` | 例外を投げます | `querySelector()` / `querySelectorAll()` と `getElementById()` は動作します。`getElementsByClassName()` も動作しますが、ライブな `HTMLCollection` ではなく静的なコレクションを返します |
| `document.activeElement` | 常に `undefined` です | `onFocus` / `onBlur` でフォーカスを追跡してください |
| `canvas` | 何もレンダリングされず、エラーも出ません | SVG、またはオフスクリーンで描画して、その結果を `img` 要素で表示します |
| `createPortal(node, document.body)` | `isConnected` は成功を報告しますが、何もレンダリングされません | `position: absolute` によるインラインのオーバーレイを使うか、ライブラリに自前のコンテナー要素を渡してください |
このポータルのギャップが原因で、Radix、Headless UI、MUI、react-select のポップオーバーはデフォルトでは何もレンダリングしません。 ほとんどのライブラリは container プロップを受け付けるので、自分がレンダリングした要素を指定してください。
### イベント
マウス、ポインター、タッチ、ドラッグ、キーボード、フォーカス、`input`/`change`/`submit`、`scroll`/`wheel`/`contextmenu`、そして `animationend`/`transitionend` はホストへ渡されます。さらに、要素ごとにいくつかのイベントも渡されます:`img` の `load`/`error`、`input`/`textarea` のクリップボードおよびコンポジション、`video`/`audio` のメディア、`details`/`dialog` の `toggle` などです。 それ以外(`onAuxClick`、`onSelect`、`onInvalid`、`onReset`、`onAnimationStart`、pointer capture、`img` の `onLoad`)は、警告なしに破棄されます。
`document.addEventListener()` と `window.addEventListener()` はエラーなく登録されますが、一度も発火しません。そのため、ドラッグは開始した要素からポインターが離れた瞬間に止まります。 `event.preventDefault()` も渡されません。フォーム送信、`dragover`/`drop`、およびリンクのクリックは、すでに保護されています。
### 属性とスタイリング
各要素は自分自身のプロパティをホスト DOM にフォワードします(`a` の `href`、`img` の `src`/`alt`、`input` の `value`/`placeholder`/`disabled` など)。さらに、すべての要素に共通のプロパティセットもフォワードされます:`id`、`className`、`style`、`title`、`tabIndex`、`role`、`draggable`、および任意の `aria-*` / `data-*` 属性(ハイフン区切りのため、`ariaLabel` は破棄されます)。 それ以外のものはすべて暗黙的に破棄されるため、カスタム状態は `data-*` として表現してください。
コンポーネントの CSS(`import './styles.css'`、CSS-in-JS、または `style` 要素のいずれであっても)は、ホストページの `head` に **スコープなし(unscoped)** で挿入されます。 そのため、クラス名が Twenty 独自のクラス名と衝突します(プレフィックスを付け、決して素の `div { ... }` セレクタは書かないでください)。また、`@media` はウィジェットではなくブラウザウィンドウに対してマッチします(独自の `container-type` を指定した `@container` を使用してください)。 インラインの `style` props には影響しません。
### ストレージとネットワーク
`localStorage` と `sessionStorage` はブラウザではなく Twenty によって提供されます。コンポーネントは不透明なオリジンのワーカー内で実行されるため、ホストがアプリに代わって値を保存します。 スコープと制限については、[storage](#storage) を参照してください。 IndexedDB、Cookie、Cache API、および `BroadcastChannel` は引き続き利用できません。 デバイス間で状態を永続化するには、[logic function](/l/ja/developers/extend/apps/logic/logic-functions)を呼び出し、その[key-value store](/l/ja/developers/extend/apps/logic/key-value-store)を使用してください。
`fetch` は動作しますが、いくつか注意点があります。
* Twenty API への呼び出しやアプリのルートへの呼び出しはホストによってプロキシされるため、[`RestApiClient`](#calling-the-twenty-rest-api) を優先して使用してください。 プロキシされた呼び出しでは、`AbortSignal` などの `RequestInit` オプションは破棄され、`string` と `URLSearchParams` のボディのみがサポートされます。
* その他のオリジンからのリクエストは、`Origin: null` を付けてサンドボックスの外へ送信されるため、サードパーティ API は `Access-Control-Allow-Origin: *` を返す場合にのみ応答します。 代わりにロジック関数から呼び出してください。
* `fetch('/rest/people')` は、サンドボックスに相対パスを解決するためのページ URL がないため、Twenty API には決してマッチしません。
### メディアキャプチャ
`navigator.mediaDevices.getUserMedia()` と `MediaRecorder` は、サンドボックス polyfill を通じて front コンポーネント内で動作するため、標準的な録画コードは変更なしで実行でき、一般的なコンテナ/コーデックの組み合わせに対しては `MediaRecorder.isTypeSupported` が応答します。 詳細な `getUserMedia` 制約オブジェクトは受け付けられますが転送はされません。ホストは、要求された種類に対してデフォルト設定でキャプチャを行い、アプリケーション全体を通して同時にライブにできるキャプチャは 1 つだけです。 記録された `Blob` を `uploadFile` ホスト関数で保存します。
### その他のギャップ
* **ファイル内容。** type が `file` の `input` は、ハンドラーにファイルのメタデータのみを渡し、バイト列は渡さないため、`FileReader` は利用できません。 `MediaRecorder` で生成されたものなど、コードですでに保持している `Blob` をアップロードするには、`uploadFile` ホスト関数を使用します。
* **ドラッグ&ドロップのペイロード。** ドラッグイベントは発火しますが、`event.dataTransfer` は `undefined` です。
* **Node 組み込みモジュール。** `fs`、`path`、`node:crypto` はビルドに失敗するため、その処理は [ロジック関数](/l/ja/developers/extend/apps/logic/logic-functions) に移動してください。 Web Crypto、`fetch`、`TextEncoder`、および `URL` は利用可能です。
* **`iframe`** は常に `allow-same-origin` なしで再サンドボックス化されるため、独自のセッションに依存する埋め込みは、ログアウト状態としてレンダリングされます。 また、`onLoad` もありません。

View File

@@ -0,0 +1,46 @@
---
title: ナビゲーションメニュー項目
description: ワークスペースのサイドバーにカスタムエントリを追加します。保存済みビューや外部 URL へのリンクを設定できます。
icon: bars
---
**ナビゲーションメニュー項目**は、左サイドバー内の 1 つのエントリです。 カスタムのサイドバーリンクを提供するには `defineNavigationMenuItem()` を使用します。通常は、提供する各[ビュー](/l/ja/developers/extend/apps/layout/views)ごとに 1 つずつ、または外部 URL を指すリンクとして使用します。
```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,
});
```
## 主なポイント
* `type` は、そのメニュー項目がどこにリンクするかを決定します。 各 `type` は、特定の識別子フィールドと組み合わされます。
| タイプ | 機能 | 必須フィールド |
| ------------------------------------ | --------------------------- | ------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | 保存済みビューを開きます | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | 外部 URL を開きます | `link` |
| `NavigationMenuItemType.FOLDER` | ネストされた項目をラベルの下にグループ化します | `name`(子項目は `folderUniversalIdentifier` を通じてフォルダを参照します) |
| `NavigationMenuItemType.OBJECT` | オブジェクトのデフォルトのインデックスページを開きます | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | スタンドアロンのページレイアウトを開きます | `pageLayoutUniversalIdentifier` |
* `position` はサイドバーでの表示順を制御します。
* enum には、ユーザーが作成したレコードのお気に入りを内部的に扱うために使用される `NavigationMenuItemType.RECORD` も含まれています。これはアプリのマニフェストからは使用できません(レコードを参照するフィールドが存在しません)。
* `icon` と `color` は任意で、エントリの見た目をカスタマイズします。
* `folderUniversalIdentifier` は、任意の項目で利用でき、その項目を `FOLDER` タイプの親の内側にネストするために使用します。
<Note>
**よくある落とし穴:** ナビゲーションメニュー項目が関連付けられていないオブジェクトを作成すると、そのオブジェクトはサイドバーからアクセスできなくなります。 技術的/内部的なオブジェクトでない限り、すべてのカスタムオブジェクトには、それを指すサイドバーエントリを用意する必要があります。
</Note>

View File

@@ -0,0 +1,59 @@
---
title: 概要
description: Twenty の UI 内にアプリを配置します — サイドバー項目、保存済みビュー、レコードページのタブ、およびサンドボックス化された React コンポーネント。
icon: table-columns
---
Twenty アプリの **レイアウトレイヤー** とは、ユーザーに見えるすべての要素を指します。アプリがサイドバーのどこに表示されるか、どのリストビューを提供するか、レコード詳細ページがどのように構成されるか、そしてそれらのページ内でどのカスタム React コンポーネントがレンダリングされるかなどです。
```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 └─────────────────────┘
```
## このセクションについて
<CardGroup cols={2}>
<Card title="ビュー" icon="list" href="/l/ja/developers/extend/apps/layout/views">
`defineView` — 表示カラム、フィルター、グループなどを含む、保存済みリスト構成。
</Card>
<Card title="ナビゲーションメニュー項目" icon="bars" href="/l/ja/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — ビューまたは外部 URL を指すサイドバー項目。
</Card>
<Card title="ページレイアウト" icon="table-columns" href="/l/ja/developers/extend/apps/layout/page-layouts">
`definePageLayout` および `definePageLayoutTab` — レコード詳細ページ上のタブとウィジェット。
</Card>
<Card title="フロントコンポーネント" icon="window-maximize" href="/l/ja/developers/extend/apps/layout/front-components">
`defineFrontComponent` — Twenty 内でレンダリングされる、サンドボックス化された React コンポーネント。
</Card>
<Card title="コマンドメニュー項目" icon="terminal" href="/l/ja/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — フロントコンポーネントを Cmd+K の項目およびクイックアクションとして登録します。
</Card>
</CardGroup>
## アプリが表示される場所
| 表示場所 | 制御内容 | エンティティ |
| -------------------- | ------------------------------------------------ | ----------------------------------------- |
| **サイドバー** | 保存済みビューまたは外部 URL へリンクするカスタム項目 | `defineNavigationMenuItem` |
| **レコードリスト** | オブジェクト向けの保存済み構成 — 表示カラム、順序、フィルター、グループ | `defineView` |
| **レコード詳細ページ** | レコードページ上のタブおよびウィジェット(独自オブジェクトのもの、または標準オブジェクトのもの) | `definePageLayout`, `definePageLayoutTab` |
| **上記いずれかの内部** | カスタム React ウィジェット — ボタン、フォーム、ダッシュボード、連携機能 | `defineFrontComponent` |
| **アプリ設定** | アプリの「設定」タブ内にある、デフォルトの変数 UI に代わるカスタム設定セクション | `defineSettingsFrontComponent` |
| **コマンドメニュー (Cmd+K)** | ピン留めされたクイックアクションまたは非表示コマンド | `defineCommandMenuItem` |
フロントコンポーネントは Remote DOM を使用して、分離された Web Worker 内で実行されます。ページ内に *ネイティブに*(iframe 内ではなく)レンダリングされますが、ホストページや DOM へ直接アクセスすることはできません。 Twenty との通信は、メッセージパッシング型のホスト API を通じて行われます。
これらは依然として積極的に開発中です。サンドボックスは DOM を部分的にのみ実装しているため、高度な利用方法では失敗する場合があります。 [現在の制限](/l/ja/developers/extend/apps/layout/front-components#current-limitations) を参照してください。

View File

@@ -0,0 +1,158 @@
---
title: ページレイアウト
description: definePageLayout と definePageLayoutTab を使用して、レコードの詳細ページのカスタマイズ(タブ、ウィジェット、front components のレンダー位置)を行います。
icon: table-columns
---
**ページレイアウト**は、レコードの詳細ページがどのように構成されるかを制御します。どのタブを表示し、そのタブにどのウィジェットを含めるかを決定します。 自分が所有するオブジェクトに対してレイアウトを宣言するには `definePageLayout()` を使用し、既に存在するレイアウト(自分のもの、または標準の Twenty のレイアウト)に単一のタブを追加するには `definePageLayoutTab()` を使用します。
| ユースケース | エンティティ |
| ----------------------------------------- | --------------------- |
| 自分が所有するオブジェクト上のレコードページ全体のレイアウトを定義する | `definePageLayout` |
| 既存のレイアウト(自分のオブジェクト、または標準オブジェクト)にタブを1つ追加する | `definePageLayoutTab` |
## definePageLayout
自分が詳細ページ全体を所有している場合に使用します。通常は、自分で定義したカスタムオブジェクトに対して使用します。
```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,
},
},
],
},
],
});
```
### 主なポイント
* `type` は `'RECORD_INDEX'`、`'RECORD_PAGE'`、`'DASHBOARD'`、`'STANDALONE_PAGE'` のいずれかです。 特定のオブジェクトの詳細ビューをカスタマイズするには `'RECORD_PAGE'` を使用します。
* `objectUniversalIdentifier` は、このレイアウトを適用するオブジェクトを指定します。
* 各 `tab` は、`title`、`position`、`layoutMode` を持つページのセクションを定義します。レコードページおよびスタンドアロンページには `VERTICAL_LIST`、ダッシュボードには `GRID`、タブのビューポートを埋める単一のウィジェットには `CANVAS` を使用します。 `VERTICAL_LIST` タブでは、ウィジェットが縦方向に積み重ねられます。 タイムライン、ファイル、メモ、タスク、ワークフローなど、独自のスクロールを管理する組み込みウィジェットは 1 つのビューポートを埋めます。フィールド、フロントコンポーネント、グラフ、その他のコンテンツに合わせたウィジェットは、そのコンテンツの高さまたは設定された高さでレンダリングされます。 `GRID` タブでは、ウィジェットは常に 12 列グリッド上のカードとして配置されます。 `CANVAS` ウィジェットには明示的な位置がありません。キャンバスタブに複数のウィジェットが含まれる場合、それらはビューポート全体を埋めるのではなく、コンテンツの高さでレンダリングされます。
* `layoutMode` を明示的に設定します。 `layoutMode` を省略すると、`STANDALONE_PAGE` では `VERTICAL_LIST`、それ以外では `GRID` が適用されますが、これはレコードページで望ましい動作であることはほとんどありません。
* タブ内の各 `widget` は、[front component](/l/ja/developers/extend/apps/layout/front-components)、リレーションリスト、その他のビルトインウィジェットタイプをレンダリングできます。
* `FRONT_COMPONENT` ウィジェットでは、同じアプリのコマンドメニュー項目ユニバーサル識別子を順序付き配列として `headerCommandMenuItemUniversalIdentifiers` に設定できます。 これらのアクションはウィジェットカードヘッダーにアイコンボタンとして表示され、コマンドレベルの可用性チェックと権限チェックが維持されます。 識別子は一意である必要があり、アプリのインストール時に解決される必要があります。
* タブの `position` は表示順を制御します。 組み込みタブの後にカスタムタブを配置するには、より大きな値(例:50)を使用します。
### フィールド ウィジェット
`FIELD` ウィジェットは、レコードの 1 つのフィールドをレンダリングします。 リレーションフィールドの場合、関連レコードの一覧を埋め込むこともできます。
```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` は、レイアウトのオブジェクト上のフィールドのユニバーサル識別子を受け取ります。
* `fieldDisplayMode` は `'FIELD'`、`'CARD'`、`'EDITOR'`、`'VIEW'`、`'TABLE'` のいずれかです。 `TABLE` は、1 対多のリレーションフィールドのレコードを一覧表示するビューを埋め込みます。
* `nestedRelationFieldMetadataId` は任意で、リレーション先オブジェクト上の 1 対多リレーションフィールドのユニバーサル識別子を受け取り、2 ホップ先のリレーションにあるレコードを一覧表示するために使用されます(例: 会社ページでその会社の担当者が持つ商談を一覧表示する、または人物ページでその人物の所属する会社の商談を一覧表示する、など)。 最初のホップは 1 対多または多対 1 のリレーションフィールドのどちらでもかまいませんが、2 つ目は必ず 1 対多でなければなりません(ジャンクションリレーションはサポートされません)。また、`fieldDisplayMode: 'TABLE'` が必須であり、ネストされたウィジェットは常に埋め込みビューとしてレンダリングされるため、これを他の表示モードと組み合わせると検証エラーになります。
## definePageLayoutTab
既存のレイアウトにタブを**追加**したい場合にのみ使用します。たとえば、標準の Company ページにアナリティクスタブを追加する場合や、自分のオブジェクトのレイアウトに AI サマリータブを追加する場合などです。
```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,
},
},
],
});
```
### 主なポイント
* `pageLayoutUniversalIdentifier` は**必須**であり、インストール時点ですでに存在するページレイアウトを指している必要があります。標準の Twenty のレイアウト、または自分のアプリで定義したレイアウトのいずれかです。 別のインストール済みアプリが所有するレイアウトへのアプリ間参照は、現時点ではサポートされていません。 親レイアウトが存在しない場合、インストールは明確な検証エラーとともに失敗します。
* 標準の Twenty レイアウトの場合は、`twenty-sdk/define` から識別子を import します:
```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
// …
```
各レイアウトエントリは、その `tabs` とその `widgets` も公開しているため、任意の階層を参照できます:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
短いエイリアス `STANDARD_PAGE_LAYOUT` も利用できます:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` はこのタブのみにスコープされます。`definePageLayout` 内でインライン定義されたウィジェットとまったく同様に、[front components](/l/ja/developers/extend/apps/layout/front-components)、views などを参照します。
* `position` は、対象のレイアウト上にある既存のタブとの順序を制御します。 組み込みタブとの相対位置が希望どおりになるような値を選択してください。
* 既存のレイアウトに追加だけを行いたい場合は、`definePageLayout` の代わりにこちらを使用してください。 レイアウト全体を自分が所有している場合は、`definePageLayout` を使用してください。

View File

@@ -0,0 +1,113 @@
---
title: ビュー
description: アプリのオブジェクト向けに、列の順序、フィルター、グループが事前設定済みの保存ビューを提供します。
icon: list
---
**ビュー**とは、オブジェクトのレコードをどのように表示するかについての保存された設定です。どのフィールドを表示するか、その順序、表示・非表示、および適用されるフィルターやグループなどを含みます。 `defineView()` を使用して、事前設定済みのビューをアプリに同梱します。
すべてのオブジェクトには、サーバーによってプロビジョニングされるメインのリストビュー(`OBJECT` ナビゲーション項目で開かれる「All ...」テーブル)が自動的に用意されます。これには、アプリが作成するオブジェクトも含まれ、アプリが宣言する各フィールドごとに 1 列が用意されます。 `defineView()` で定義したビューは、そのデフォルトビューに加えて常に**追加の**ビューとして扱われます。
```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,
},
],
});
```
## 主なポイント
* `objectUniversalIdentifier` は、このビューを適用するオブジェクトを指定します。 定義したカスタムオブジェクトでも、Twenty の標準オブジェクトでも可能です。
* オブジェクトのメインのリストビューはサーバー所有です。`key` は非推奨で無視されるため、マニフェストビューがそれを取得することはできません。 サイドバーから 1 クリックでビューにアクセスできるようにしたい場合は、`VIEW` ナビゲーション項目を追加してください。
* `fields` は、どの列をどの順序で表示するかを制御します。 各フィールドは `fieldMetadataUniversalIdentifier` を参照します。 `createdAt` などの自動作成システムフィールドの参照方法については、[システムフィールドのターゲット指定](/l/ja/developers/extend/apps/data/system-fields)を参照してください。
* さらに高度な構成のために、`filters`、`filterGroups`、`sorts`、`groups`、`fieldGroups` も定義できます。
* 同じオブジェクトに複数のビューがある場合、`position` が表示順を制御します。
## オプションのプロパティ
| プロパティ | 値 | 説明 |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (デフォルト), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | レコードのレイアウト方法。 (`FIELDS_WIDGET`、`TABLE_WIDGET`、`KANBAN_WIDGET`、`LIST_WIDGET`、`CALENDAR_WIDGET` も存在しますが、ページレイアウトウィジェットによって内部的に使用されます。) |
| `visibility` | `ViewVisibility.WORKSPACE` (デフォルト), `ViewVisibility.UNLISTED` | ビューがワークスペース全体で一覧表示されるか、ピッカーから非表示にされるか。 |
| `openRecordIn` | 非推奨 | 参照されなくなりました: レコードをどこで開くかは、現在では[object](/l/ja/developers/extend/apps/data/objects)のプロパティ(`defineObject()` の `openRecordIn`)となっており、設定されていない場合は各メンバーの設定が使用されます。 |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | デフォルトのソート順。 |
| `isCompact` | `boolean` | 行のコンパクト表示。 |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | レコードをフィールドでグループ化します(例: かんばんのカラム)。 |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | かんばんカラムの集計とサイズ設定。 |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | カレンダービュー: レイアウト、レコードの位置を決める日付フィールド、オプションの終了日フィールド。 |
上記のすべての enum は `twenty-sdk/define` からエクスポートされています。
## フィルター
ビューには、あらかじめフィルターを適用した状態で提供できます。 各フィルターには 3 つの要素があります: フィルタリング対象の**フィールド**、**オペランド**(どのように比較するか)、**値**(何と比較するか)。 この 3 つがすべてそろっている必要があります — フィールドの型に適用できないオペランドを使用すると、同期時に拒否されます。
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### フィールドタイプごとにサポートされているオペランド
| フィールドタイプ | サポートされるオペランド |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `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` |
> 名前が似ているフィールドタイプでも、まったく異なるオペランドを使用する場合があります。一般的な例としては `SELECT` と `MULTI_SELECT` があります。
### オペランドごとの値の形式
`value` フィールドは常に JSON シリアライズ可能な値ですが、想定される形式はオペランドによって異なります。
| オペランドの種類 | 値の形式 | 例 |
| ------------------------------------------------------- | ---------------------- | ------------------------ |
| `SELECT` に対する `IS`, `IS_NOT` | オプションキー(文字列)の配列 | `['ACTIVE', 'PENDING']` |
| `MULTI_SELECT` に対する `CONTAINS`, `DOES_NOT_CONTAIN` | オプションキー(文字列)の配列 | `['TAG_A']` |
| `RELATION` に対する `IS`, `IS_NOT` | レコード ID(UUID)の配列 | `['c5a1...']` |
| テキスト系フィールドに対する `CONTAINS`, `DOES_NOT_CONTAIN` | 文字列 | `'acme'` |
| `NUMBER` に対する `IS`, `IS_NOT` | 文字列(値) | `'5'` |
| `RATING` / `UUID` に対する `IS` | 文字列(値) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | 文字列(境界値) | `'10'` |
| `DATE` / `DATE_TIME` に対する `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 文字列 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | 空の文字列 | `''` |
| `BOOLEAN` に対する `IS` | `'true'` または `'false'` | `'true'` |
## ビューが UI にどのように表示されるか
ビュー単体では、サイドバーから直接アクセスすることはできません。 それをサイドバーに表示するには、ビューの `universalIdentifier` を指す、種類が `VIEW` の[ナビゲーションメニュー項目](/l/ja/developers/extend/apps/layout/navigation-menu-items)と組み合わせます。 それが標準的なパターンです。各カスタムオブジェクトには、作成時にデフォルトのインデックスビューが自動生成されるため、それを開くサイドバーエントリだけを用意しておけば十分です。