diff --git a/content/docs/build/automation/approvals.ja.mdx b/content/docs/build/automation/approvals.ja.mdx new file mode 100644 index 00000000..c0d4b0ef --- /dev/null +++ b/content/docs/build/automation/approvals.ja.mdx @@ -0,0 +1,279 @@ +--- +title: 承認 +seoTitle: "承認フロー:レコードを人による承認へ回す" +description: レコードを人による承認へ回します。自動化が行レベルセキュリティを黙ってすり抜けることはさせません。 +editions: [community, cloud, enterprise] +translation: + source_sha: e03dd11f32910f971d7514f12aab8e3c2ac1eceb25966b2b4162185eb062a521 + guide_rev: 1 + mode: auto +--- + +**承認とは、承認ノードを持つ[フロー](/docs/build/automation/flows)です。フローは人が承認または却下するまで一時停止し、その後、対応する分岐に沿って再開します。** 新たに覚えるべき独立した承認エンジンはありません。トリガー、分岐、エラー処理は、ほかのフローとまったく同じように動作します。このページが加えるのは承認ノードそのものと、ルーティングと同じくらい重要な 2 つのアクセスの判断です。 + +## 誰が何をするか [#who-does-what] + +意図的に分けられた 3 つのロールです。 + +| ロール | できること | 必要なもの | +|---|---|---| +| **ビルダー** | 承認フローを作成、編集する | `manage_metadata`(通常は Studio のユーザー) | +| **申請者** | フローに入るレコードを申請する | 通常のレコードアクセス。自動化の設定は不要 | +| **承認者** | 承認依頼に対応する(承認 / 却下) | 権限セットで付与された機能 | + +エンドユーザーはレコードを申請し、依頼に対応します。自動化を編集することはありません。自動化の設定画面は、利用者向けのアプリに含めないでください。 + +## 誰として実行するか:安全上の判断 [#as-whom-does-it-run--the-safety-decision] + +フローは `runAs` を宣言します。承認にとって、これはフローが行レベルセキュリティを黙ってすり抜けないようにするための判断です。 + +| `runAs` | データ操作を実行する主体 | 使う場面 | +|---|---|---| +| `'user'`(デフォルト) | **申請者**。その RLS に従います | フローが、申請者がすでに見られるレコードにだけ触れる場合 | +| `'system'` | **昇格**。RLS をすり抜けます | 申請者が見られないレコードをフローが読み書きしなければならない場合(元帳への記帳、申請者が見られない行を所有する承認者への通知) | + +昇格は、偶然ではなく見える形にするために**明示的に**宣言します。デフォルトが `'user'` なので、承認フローが申請者に、テナントや所有者をまたぐ到達範囲を黙って与えることはできません。昇格はオプトインで、監査できます。 + +> **ヒント:** スケジュールで起動するエスカレーションには起動したユーザーがいないので、そもそも動作させるには `runAs: 'system'` でなければなりません。これが昇格の正当なケースです。「動かすためにどこでも `system`」はアンチパターンです。 + +## 承認ノード [#the-approval-node] + +ノードは**誰が承認するか**と、その判断をどう集約するかを宣言します。 + +```ts +// One flow node; the flow around it is omitted (see the complete flow below). +{ + id: 'manager_approval', + type: 'approval', + label: 'Manager Approval', + config: { + approvers: [{ type: 'field', value: 'owner_manager_id' }], + behavior: 'unanimous', + approvalStatusField: 'approval_status', + lockRecord: true, + }, +} +``` + +| 設定 | 働き | +|---|---| +| `approvers` | 誰が判断しなければならないか。下記の承認者の種類を参照 | +| `behavior` | 複数の承認者をどう集約するか:`'first_response'`(デフォルト)、`'unanimous'`、`'quorum'`、`'per_group'` | +| `minApprovals` | 必要な承認数。`quorum` では合計(N 人中 M 人)、`per_group` ではグループごと(デフォルト 1) | +| `onEmptyApprovers` | 承認者の解決結果が誰もいなかったときの動作:`'admin_rescue'`(デフォルト)、`'fail'`、`'auto_approve'` | +| `decisionOutputs` | 判断が構造化された出力としてフローに持ち帰れるキー | +| `approvalStatusField` | プラグインが依頼のステータスを反映する、省略可能なレコードのフィールド | +| `lockRecord` | 依頼が保留中の間、レコードを編集からロックします | + +`approvers` の各エントリは `type` と `value` を指定します。 + +| `type` | 解決先 | +|---|---| +| `manager` | 申請者のマネージャー(`sys_user.manager_id`)。value は取りません | +| `position` | 組織のポジションの保持者 | +| `department` | 事業単位**とそのすべての下位単位**のメンバー | +| `team` | フラットなコラボレーションチームのメンバー | +| `field` | 起動したレコードのフィールドが保持するユーザー ID | +| `user` | 名前で指定した 1 人のユーザー ID | +| `org_membership_level` | [組織メンバーシップの階層](/docs/configure/users#organization-membership-tiers)(`owner` / `admin` / `delegated_admin` / `member`) | +| `expression` | ノードに入るときに解決される CEL 式。下記を参照 | + +文字どおりの `user` ID よりも、間接的なバインディング(`manager`、`position`、`department`、`team`)を優先してください。本人がチームを移ったり退職したりしても機能し続けますが、ハードコードされた ID は、もう承認を見るべきではない人に黙って承認を回してしまいます。 + +> **警告:`position` とメンバーシップの階層。** `{ type: 'position', value: 'finance_manager' }` はポジション(`sys_user_position`)の保持者に回します。[組織メンバーシップの階層](/docs/configure/users#organization-membership-tiers)(`sys_member.role`:`owner`/`admin`/`delegated_admin`/`member`)は 16.0 以降 `type: 'org_membership_level'` で指定します。旧来の表記 `role` は **1 リリースの間だけの非推奨エイリアス**で、警告付きで今も同じように読み込まれて解決されますが、次のメジャーで削除されます。メンバーシップ階層の種類として書かれたポジション名は誰にも一致せず、依頼は止まってしまいます。`os lint` は両方のケースを指摘します(`approval-approver-not-membership-tier`、`approval-approver-type-deprecated`)。value が組織のポジションを指している場合、修正は `type: 'position'` です。 + +### クォーラムとグループごとの承認(16.0) [#quorum-and-per-group-sign-off-160] + +`first_response` と `unanimous` に加えて、16.0 では 2 つの集約モードが追加されました。**クォーラム**は N 人中 M 人の承認で確定します。 + +```ts +// The `config` of an `approval` node; the node around it is omitted. +config: { + approvers: [ + { type: 'user', value: 'director_a' }, + { type: 'user', value: 'director_b' }, + { type: 'user', value: 'director_c' }, + ], + behavior: 'quorum', + minApprovals: 2, // any 2 of the 3 approve +} +``` + +**グループごと**は、ラベル付けされた*各*グループからの承認(会签)を必要とします。承認者に `group` でラベルを付けると、すべてのグループが `minApprovals` 件の承認(デフォルトはグループごとに 1)を得たときにだけノードが先に進みます。 + +```ts +// The `config` of an `approval` node; the node around it is omitted. +config: { + approvers: [ + { type: 'position', value: 'legal_counsel', group: 'legal' }, + { type: 'position', value: 'finance_manager', group: 'finance' }, + ], + behavior: 'per_group', // one sign-off from legal AND one from finance +} +``` + +すべてのモードに共通する意味: + +- 承認者の集合は、不在時の代理を含む**依頼開始時のスナップショット**に照らして集計されます。誰が応答しなければならないかは依頼が開始されたときに確定し、不在の承認者は依頼を止めるのではなく代理に置き換えられます。 +- **1 件の却下はやはり拒否権です**。どのモードでも、ノードを `rejected` として確定させます。 +- しきい値は実行時に、解決できる承認者の数に**切り詰められる**ので、誤設定された `minApprovals` が依頼をデッドロックさせることはありません。 +- `group` ラベルのない承認者はそれぞれが独自のグループになるので、ラベルなしの承認者リストでも `per_group` のもとで妥当に動作します。 + +### 動的なルーティング:承認者が次の承認者を選ぶ(17.0) [#dynamic-routing-an-approver-picks-the-next-approver-170] + +17.0 で追加された 3 つの機能により、ノードは固定の宣言からではなく、実行がそれまでに生み出したものから承認者を決められます。 + +**`expression` 承認者**は、ノードに入るときに、3 つの明示的なルートに対して CEL 式を解決します。`current.*`(レコードの現在の状態)、`trigger.*`(申請時のスナップショット)、`vars.*`(上流ノードの出力を含むフロー変数)です。結果が承認者の顔ぶれになります。 + +```ts +// The `config` of an `approval` node; the node around it is omitted. +config: { + approvers: [ + { + type: 'expression', + value: 'vars.legal_review.picked_departments', + resolveAs: 'department', // each value is a department id, fanned out to its members + }, + ], +} +``` + +`resolveAs` は `expression` 承認者自身のキー(デフォルトは `'user'`、または `'department'` / `'position'` / `'team'`)で、式が返した ID の種類を示し、静的な承認者の種類と同じグラフ検索で各 ID を展開します。`behavior: 'per_group'` のもとでは、返された各値が独自のグループになり、返された部署ごとに 1 件の承認が必要になります。 + +`record` と素のフィールド名は意図的に**使えません**。プラットフォームのほかのすべての場面で `record` は「イベント時点のレコード」を意味しており、ここで再利用すると 2 つの時点のどちらかを黙って指してしまうからです。 + +上のような式に値を供給するのが **`decisionOutputs`** です。判断が持ち帰れるキーは*作成者*が宣言し、承認者は値を埋めるだけです。受け付けられた出力は `.` というフロー変数として実行を再開させます。素の名前には決してならないので、承認者が作成者の変数を覆い隠すことはできません。 + +```ts +// One flow node; the flow around it is omitted. +{ + id: 'legal_review', + type: 'approval', + label: 'Legal Review', + config: { + approvers: [{ type: 'position', value: 'legal_counsel' }], + decisionOutputs: [ + { key: 'picked_departments', type: 'department', multiple: true }, + ], + }, +} +``` + +エントリは、素のキー(判断の UI ではプレーンなテキスト入力としてレンダリングされます)か、型付きの宣言 `{ key, type, multiple }` のいずれかです。型付きの宣言は、判断の UI に代わりにレコードピッカーをレンダリングさせます。`user` / `department` / `position` / `team` はそれぞれ対応するシステムオブジェクトのピッカーになり、`multiple: true` は ID の配列を集めます。宣言されていないキーを持つ判断は拒否されます。`decision` と `requestId` は予約されています。 + +**`onEmptyApprovers`** は、解決結果に具体的な人が 1 人もいないとき(式の結果が空、ポジションに人がいない、複数選択フィールドが空)の動作を決めます。 + +| 値 | 動作 | +|---|---| +| `'admin_rescue'`(デフォルト) | とにかく依頼を開始し、大きく警告します。特権を持つ管理者が Reassign で引き継げます。レコードを素通りさせることも、実行を止めることもしない唯一の選択肢です | +| `'fail'` | ノードが失敗します(fault エッジ / 実行の失敗)。顔ぶれが空なら設定のバグとしか考えられない場合に選びます | +| `'auto_approve'` | 依頼をスキップし、`output.autoApproved = true` とともに `approve` エッジに沿って続行します。レコードを黙って素通りさせるので、オプトインです | + +このデフォルトは意図的なものです。承認の役目は門番なので、空の顔ぶれが黙って通ることがあってはなりません。 + +## 完全な承認フロー [#a-complete-approval-flow] + +大型の提案を所有者のマネージャーに回し、その判断で分岐します。 + +```ts +export const opportunityApproval = defineFlow({ + name: 'opportunity_approval', + label: 'Opportunity Approval', + type: 'record_change', + status: 'active', + runAs: 'user', // submitter's RLS unless a step genuinely needs more + nodes: [ + { + id: 'start', + type: 'start', + label: 'Proposal over 50,000', + config: { + triggerType: 'record-after-update', + objectName: 'opportunity', + condition: "record.amount >= 50000 && record.stage == 'proposal'", + }, + }, + { + id: 'manager_approval', + type: 'approval', + label: 'Manager Approval', + config: { + approvers: [{ type: 'field', value: 'owner_manager_id' }], + behavior: 'unanimous', + approvalStatusField: 'approval_status', + lockRecord: true, + }, + }, + { + id: 'mark_approved', + type: 'update_record', + label: 'Mark Approved', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { stage: 'negotiation' }, + }, + }, + { + id: 'mark_rejected', + type: 'update_record', + label: 'Mark Rejected', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { stage: 'closed_lost' }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'manager_approval' }, + { id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' }, + { id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' }, + { id: 'e4', source: 'mark_approved', target: 'end' }, + { id: 'e5', source: 'mark_rejected', target: 'end' }, + ], +}); +``` + +ラベル付きのエッジに注目してください。`approve` と `reject` が判断後の分岐に名前を付けています。却下の経路は必ずモデル化してください。承認の分岐しかないフローでは、却下されたレコードが行き場を失います。 + +**多段階の承認:** 複数の `approval` ノードをつなぎます。**並行承認:** [フロー](/docs/build/automation/flows)の集約ノードのパターンを参照してください。 + +## 承認者が体験すること [#what-the-approver-experiences] + +永続的な承認の状態は `@objectstack/plugin-approvals` パッケージが管理します。依頼が保留中の間: + +1. プラグインは依頼(`sys_approval_request`)と、それに対して行われたすべての判断(`sys_approval_action`)を永続化します。承認の履歴はクエリできるデータです。 +2. `lockRecord: true` の場合、判断が下るまでレコードは編集からロックされます。 +3. `approvalStatusField` を設定すると、レコード自身のフィールドが依頼のステータスを反映するので、ビューやレポートでそれを使ってフィルターできます。 +4. 承認者が承認または却下すると、プラグインは一時停止していたフローを対応するエッジに沿って再開します。16.0 以降、判断には**添付ファイル**を付けられます(`sys_approval_action.attachments` に永続化され、decide/comment のルートとクライアント SDK を通じて受け渡されます)。署名済みの PDF や証拠は判断とともに保管されます。 +5. 依頼はサーバーで計算された **`decision_progress`** を公開します。`unanimous`/`quorum` では得た承認数と必要な承認数、`per_group` ではグループごとの内訳です。そのため受信トレイや独自の UI は、集計を再実装せずに進捗をレンダリングできます。 + +**依頼を見られるのは誰か(17.0)。** 依頼はレコードのスナップショット、判断の履歴全体、添付ファイルを持つので、読めるのはその参加者だけです。申請者、現在の承認者(依頼が待っているポジションの保持者を含む)、そしてすでに対応またはコメントした人です。プラットフォームと組織の管理者はすべての依頼を見られます。それ以外の人には何も見えず、レコードを読めるかどうかはそれを変えません。承認者で依頼リストをフィルターするのはフィルターであって付与ではありません。それを省くと、テナント全体ではなく、自分が参加している依頼だけが返されます。見える依頼の中でも、スナップショットからは、読み手がそのレコードで読めないフィールドがすべて取り除かれます。17.6 以降は、マスクされた状態でしか読めないフィールドもすべて取り除かれます。 + +**判断は宣言されたアクションです(16.0)。** 判断の全体(承認、却下、再割り当て、修正のための差し戻し(`/revise`)、情報の依頼、リマインド、取り消し、再申請)は、`sys_approval_request` 上の `type: 'api'` アクションとして宣言されており、型付きのパラメータ、保留中のときだけ表示するゲート、申請者か承認者かの述語(例:`record.submitter_id == ctx.user.id`)を持ちます。承認の受信トレイは、手書きのボタンではなく、これらの宣言されたアクションをレンダリングします。そのため新しい判断機能はメタデータとして出荷され、クライアントのリリースは必要ありません。 + +承認すること自体がゲート付きのアクションです。「承認してよい」は、承認者の権限セットで付与される機能(例:`approve_invoice`)としてモデル化し、承認アクションの `requiredPermissions` をそれでゲートします。そうすればゲートは画面から隠されるだけでなく、UI とサーバーの**両方**で強制されます。 + +## ベストプラクティス [#best-practices] + +| すること | しないこと | +|---|---| +| 明確な開始条件を定義する | 承認ステップを作りすぎる | +| 妥当なタイムアウト期間を設定する | 承認を複雑にしすぎる | +| 適切な場合は取り消しを許す | 却下の経路を忘れる | +| すべての関係者に通知する | 承認者をハードコードする | +| 承認の履歴を追跡する | 「承認」を UI でだけゲートする | +| デフォルトは `runAs: 'user'` にし、1 ステップずつ昇格する | 「動かすために」どこでも `runAs: 'system'` を設定する | + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|---|---| +| [フロー](/docs/build/automation/flows) | このページの土台となるフローのリファレンス:トリガー、ステップ、エラー処理 | +| [ワークフロー](/docs/build/automation/workflows) | 承認が置かれるライフサイクルを制約する | +| [アクション](/docs/build/interface/actions) | 申請と承認をインターフェースのボタンとして表示する | +| [CEL 式](/docs/reference/cel) | 開始条件の背後にある言語 | +| [メール](/docs/configure/email) | 承認者と関係者に通知する | +| [自動化の概要](/docs/build/automation) | 選び方:フロー、ワークフロー、承認 | diff --git a/content/docs/build/automation/flows.ja.mdx b/content/docs/build/automation/flows.ja.mdx new file mode 100644 index 00000000..46a7b726 --- /dev/null +++ b/content/docs/build/automation/flows.ja.mdx @@ -0,0 +1,404 @@ +--- +title: フローと自動化 +description: 宣言的なビジネスロジック。AI に説明しても TypeScript で書いても、ランタイムはどちらの場合も同じ成果物を実行します。 +editions: [community, cloud, enterprise] +translation: + source_sha: 011506676f1192c1d884573299f8404d1cf906edfc56cb81b122979ba8410d43 + guide_rev: 1 + mode: auto +--- + +フローは、サーバーを書かずにビジネスロジックを表現する方法です。すべてのフローは、ランタイムが実行する宣言的なメタデータであり、オブジェクトやビューと同じです。そのため、フローは `os diff`、監査ログ、Studio のフロービルダー、そして [AI Builder](/docs/build/ai-builder) のすべてに同時に現れます。 + +ほとんどのお客様は、AI に頼んでフローを作成します。 + +> *「優先度の高いチケットが 'new' のまま 30 分経ったら、Slack でマネージャーに通知して」* + +AI はこのページで説明する形でフローのメタデータを生成するので、読んで編集できます。 + +フローはグラフです。`nodes` が作業を行い、`edges` がそれらをつなぎ、`start` ノードがフローを起動するものを示します。この形は ObjectStack の `FlowSchema` で、ObjectStack の [Flow metadata](https://objectstack.ai/docs/automation/flows) に完全に記載されています。 + +スタックでこの機能を有効にします。 + +```ts +export default defineStack({ + // ... + requires: ['automation', 'triggers'], // the flow engine, and the triggers that start flows +}); +``` + +両方のトークンが必要です。レコードの変更、スケジュール、API 呼び出しで開始するフローがあるのに `requires` にどちらかが欠けているスタックを、`defineStack` は拒否します。そのようなフローは登録されても決して実行されないからです。 + +## フローの種類 [#flow-types] + +| `type` | 起動するもの | 用途 | +|---|---|---| +| `record_change` | レコードの挿入、更新、削除。開始ノードで結び付けます | 「チケットが作成されたら担当者にメールする」 | +| `schedule` | 開始ノード上の cron スケジュール | 「毎晩午前 2 時に古いタスクをマークする」 | +| `schedule`、相対時間 *(16.0)* | 日付フィールドと今日との近さ。毎日のスイープで判定 | 「契約終了の 60 日前に通知する」 | +| `autolaunched` | アクションボタン、別のフロー、または API 呼び出し | 「請求書を承認」アクション | +| `screen` | ユーザー。1 つ以上の画面を通じて | ガイド付きのデータ入力 | +| `api` | 開始ノードの `config.secret` で署名された HTTP リクエスト | 外部システム向けのエンドポイント | + +## レコード変更:レコードの変更に反応する [#record-change-react-to-a-record-change] + +```ts +// src/flows/ticket_assigned_email.ts +import { defineFlow } from '@objectstack/spec'; + +export const ticketAssignedEmail = defineFlow({ + name: 'ticket_assigned_email', + label: 'Email the assignee of a new ticket', + type: 'record_change', + status: 'active', + nodes: [ + { + id: 'start', + type: 'start', + label: 'Ticket created', + config: { objectName: 'support_ticket', triggerType: 'record-after-create' }, + }, + { + id: 'email_assignee', + type: 'notify', + label: 'Email the assignee', + config: { + recipients: '{record.assignee}', + title: 'New ticket: {record.subject}', + message: 'You have been assigned {record.subject}, priority {record.priority}.', + channels: ['email'], + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'email_assignee' }, + { id: 'e2', source: 'email_assignee', target: 'end' }, + ], +}); +``` + +メールは `notify` ノードを通じて送られます。独立したメールノードの種類はありません。`channels: ['email']` は、[メール](/docs/configure/email)で設定されたトランスポートを通じて、メールチャネルで通知を送ります。`channels` がなければ、通知はアプリ内の受信トレイにだけ届きます。インラインのテキストの代わりに保存済みのメールテンプレートを送るには、`title` と `message` の代わりに `template`(`sys_email_template` の名前)を設定します。 + +ノードの `config` 内のフィールド値と通知テキストはテンプレートです。`{record.}` は起動したレコードを、`{}` はフロー変数を、`{$User.Id}` は現在のユーザーを読み、`{NOW()}` / `{TODAY()}` は日付マクロです。条件は波括弧なしの素の CEL です。[条件と分岐](#conditions-and-branches)を参照してください。 + +開始ノードの `triggerType` がイベントを選び、省略可能な `condition` がそれを絞り込みます。たとえば `condition: "record.priority == 'high'"` です。 + +| `triggerType` | 発動するタイミング | +|---|---| +| `record-after-create` | 挿入の後 | +| `record-after-update` | 更新の後 | +| `record-after-delete` | 削除の後 | +| `record-after-write` | 挿入**または**更新の後 | +| `record-before-create` | 挿入の前 | +| `record-before-update` | 更新の前 | +| `record-before-delete` | 削除の前 | +| `record-before-write` | 挿入**または**更新の前 | + +更新では、`previous` が変更前のレコードを保持します:`condition: 'record.stage != previous.stage'`。同じ書き込みの中で、保存される前に保留中のレコードを変更するには、フローではなく before フックを使います。フローは保存の副作用のためのものです。レコードの作成、通知、HTTP 呼び出し、承認の依頼などです(ObjectStack の [Hooks vs flows](https://objectstack.ai/docs/automation/hooks#hooks-vs-flows))。 + +## スケジュール:時刻に従って実行する [#scheduled-run-on-a-clock] + +```ts +export const nightlyCleanup = defineFlow({ + name: 'nightly_cleanup', + label: 'Mark overdue tasks', + type: 'schedule', + status: 'active', + runAs: 'system', // a scheduled run has no user, so its data operations need an explicit identity + nodes: [ + { + id: 'start', + type: 'start', + label: 'Every night at 2am', + config: { + triggerType: 'schedule', + schedule: { type: 'cron', expression: '0 2 * * *', timezone: 'America/New_York' }, + }, + }, + { + id: 'mark_overdue', + type: 'update_record', + label: 'Mark open tasks past due', + config: { + objectName: 'task', + filter: { status: 'open', due_date: { $lt: '{TODAY()}' } }, + fields: { status: 'overdue' }, + multi: true, // update every match; without it, a filter that is not a single id is refused + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'mark_overdue' }, + { id: 'e2', source: 'mark_overdue', target: 'end' }, + ], +}); +``` + +`@objectstack/service-job` 機能に支えられています。[ランタイム機能](/docs/reference/runtime-capabilities)を参照してください。時間で起動するフローには組織を引き継ぐ呼び出し元がいません。開始ノードで `organization` を使って組織を宣言しなければならない場合については、ObjectStack の [Flow metadata](https://objectstack.ai/docs/automation/flows#the-acting-organization) に記載されています。 + +> **スケジュールされた処理は、デプロイメントが有効にするまでオフです。** ObjectStack 17.5 以降、時間で起動するフロー(`schedule` の周期、または下記の相対時間スイープ)とパッケージ化された `defineJob` の cron ジョブは、デプロイメントが `OS_AUTOMATION_SCHEDULED_WORK_ENABLED` を `true`(または `1`、`on`、`yes`)に設定したときにだけ有効になります。未設定なら何も有効にならず、そのようなフローはそれぞれエラーではなく *disabled by deployment policy* として報告されます。オフは ObjectStack ランタイムのデフォルトです。お使いの ObjectOS イメージやそのデプロイバンドルのテンプレートがこの変数を設定してくれるかどうかは、このページでは約束できません。アプリがスケジュールフローに依存するときは明示的に設定し、実効値を表示する `os doctor` で確認してください。[環境変数](/docs/reference/environment-variables#runtime-services)を参照してください。プラットフォーム自身のスケジュール処理(承認のエスカレーション、保持期間、メッセージの配信)には影響しません。`isolated` テナンシー方式では、開始ノードで `organization` を宣言していないスケジュールフローは、スイッチがオンでも有効になりません。 + +## 相対時間:日付フィールドを基準に発動する(16.0) [#time-relative-fire-relative-to-a-date-field-160] + +「契約終了の 60 日前に通知する」は、かつては `record.end_date == daysFromNow(60)` を条件とするレコード変更フローとして作成されていました。この述語はレコードが*たまたま変更された*ときにしか評価されないので、放っておくとほとんど発動しません。そのようには書かないでください。代わりに**相対時間トリガー**を宣言します。フローの開始ノードが `config.timeRelative` 記述子を持ち、ランタイムがスケジュールに従ってオブジェクトをスイープし、**一致したレコードごとに 1 回**フローを起動します。 + +```ts +export const renewalReminder = defineFlow({ + name: 'contract_renewal_reminder', + label: 'Contract renewal reminder', + type: 'schedule', + status: 'active', + runAs: 'system', // a sweep has no user to act as + nodes: [ + { + id: 'start', + type: 'start', + label: 'Start', + config: { + timeRelative: { + object: 'contracts', + dateField: 'end_date', + offsetDays: [60, 30, 7], // T-minus thresholds + filter: { status: 'active' }, // AND-ed with the date window + }, + // Optional sweep cadence — defaults to daily at 08:00 UTC. + schedule: { type: 'cron', expression: '0 8 * * *' }, + }, + }, + { + id: 'notify_owner', + type: 'notify', + label: 'Notify Owner', + config: { recipients: '{record.owner}', title: 'Contract {record.name} ends on {record.end_date}' }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'notify_owner' }, + { id: 'e2', source: 'notify_owner', target: 'end' }, + ], +}); +``` + +| キー | 宣言するもの | +|---|---| +| `object` | スイープがレコードを問い合わせるオブジェクト(マシン名) | +| `dateField` | 今日を基準に評価される `date` / `datetime` フィールド(日単位) | +| `offsetDays` | **オフセットモード**:`dateField` がちょうど今日 + 列挙した各オフセットのときに発動します(`[60, 30, 7]`。負の値は過去で、たとえば `[-1]` は翌日) | +| `withinDays` | **範囲モード**:`dateField` が今日から N 日以内である日には毎日発動します。正の値は今後(「まもなく期限切れ」)、負の値は範囲を区切った期限超過の遡り、`0` は今日が期限 | +| `filter` | 計算された日付の範囲と AND 結合される、省略可能な ObjectQL の where マップ(例:`{ status: 'active' }`) | +| `maxRecords` | 1 回のスイープで起動するレコード数の上限(デフォルト 1000。上限で切り詰めるとスイープがログに記録します) | + +`offsetDays` と `withinDays` のどちらか**一方だけ**を設定しなければなりません。ほかによくある 2 つの形です。 + +```ts +// Two variants of the start node's config.timeRelative; the rest of the flow is as above. + +// "Expiring soon" — fires every day a document is within 30 days of expiry. +timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 } + +// Overdue sweep — fires for POs up to 14 days past due (bounded lookback). +timeRelative: { object: 'purchase_order', dateField: 'due_date', + withinDays: -14, filter: { status: 'open' } } +``` + +各起動は一致したレコードを自動化コンテキストに置くので、開始ノードの `condition` と `{record.}` の埋め込みは、レコード変更フローとまったく同じように動作します。検出のクエリはシステムとして実行され、レコードごとに障害が分離されます。`os validate` には準備状況のチェックが加わり、`timeRelative.object` がスタックで定義されていないオブジェクトを指しているとき、および自動起動のフローが `draft` ステータスのまま残っているときに警告します。 + +> **同じ日のチェックは、今は問題なく使えます(#3183)。** 16.0 では `record.due_date == today()` が一致します。エンジンが時間型の `==` / `!=` 比較を変換するので、日付フィールドが `today()` と正しく比較されます。本当に「今日が期限」という条件にはこれを使い、「日付の N 日前/後」という形のものには `timeRelative` を使ってください。 + +## 手動で開始:アクションと API 呼び出し [#started-by-hand-actions-and-api-calls] + +```ts +export const approveInvoice = defineFlow({ + name: 'approve_invoice', + label: 'Approve invoice', + type: 'autolaunched', + status: 'active', + variables: [ + { name: 'invoice_id', type: 'text', isInput: true }, + ], + nodes: [ + { id: 'start', type: 'start', label: 'Start' }, + { + id: 'approve', + type: 'update_record', + label: 'Approve the invoice', + config: { + objectName: 'invoice', + filter: { id: '{invoice_id}' }, + fields: { status: 'approved', approved_by: '{$User.Id}' }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'approve' }, + { id: 'e2', source: 'approve', target: 'end' }, + ], +}); +``` + +請求書のビューで、種類が `flow` の[アクション](/docs/build/interface/actions)を使ってボタンとして表示するか、REST で呼び出します。 + +```bash +curl -X POST https://app.example.com/api/v1/automation/approve_invoice/trigger \ + -H 'Authorization: Bearer ' \ + -H 'Content-Type: application/json' \ + -d '{"params": {"invoice_id": "inv_123"}}' +``` + +入力は `isInput: true` で宣言したフロー変数で、同じ名前で `params` に入れて送ります。宣言された入力だけがバインドされます。呼び出し元のアイデンティティが実行に引き渡されるので、更新は呼び出し元の権限のもとで実行されます。 + +このルートは何が起きたかを示す HTTP ステータスで応答するので、本文の `success` フラグではなくステータスで分岐してください。ディスパッチされた実行(完了、一時停止、または[拒否](#ending-a-run-completed-or-refused))には `200`、不明なフローには `404`、オフに切り替えられたフローには `409 FLOW_DISABLED`、開始ノードのない定義には `422 FLOW_NO_START_NODE`、ディスパッチされて失敗した実行には `400 FLOW_FAILED` です。種類が `flow` のアクションも同じように応答します。[REST API → Flow status table](/docs/reference/rest-api#flow-status-table) を参照してください。 + +## ノードの種類 [#node-types] + +| ノードの `type` | 目的 | +|---|---| +| `get_record` | ObjectQL でレコードを読みます。1 件、または `limit` が 1 より大きいときはリスト | +| `create_record` / `update_record` / `delete_record` | オブジェクトに書き込みます(`multi: true` で一致したすべてを更新または削除) | +| `notify` | 通知を送ります。デフォルトはアプリ内の受信トレイ、`channels: ['email']` でメール | +| `http` | HTTP エンドポイントを呼び出します | +| `connector_action` | インストール済みのコネクタ(Slack など)でアクションを実行します | +| `script` | `config.function` で名前を指定した登録済みの関数を呼び出します | +| `decision` | エッジの条件で分岐します | +| `assignment` | フロー変数を設定します | +| `loop` | コレクションの各項目について本体の領域を 1 回ずつ実行します | +| `parallel` | 分岐の領域を並行して実行します | +| `try_catch` | 領域内の失敗を捕捉し、必要に応じて再試行します | +| `wait` | タイマーまたは名前付きシグナルまで一時停止します | +| `screen` | ユーザーの入力を待って一時停止します | +| `subflow` | 別のフローを呼び出します | +| `approval` | 承認者が判断するまでブロックします(`@objectstack/plugin-approvals` が必要) | +| `end` | 実行を終了します。デフォルトは `completed`、または理由付きの `refused`(下記) | + +### 実行の終了:完了または拒否 [#ending-a-run-completed-or-refused] + +ObjectStack 17.4 以降、`end` ノードは実行の**結果**を宣言します。断らなければならないフロー(重複、上限を超える金額など)は、失敗したり、断ったことを伝えるだけの画面を表示したりする代わりに、拒否の `end` で終わります。 + +```ts +// One flow node; the flow around it is omitted. +{ + id: 'refuse_duplicate', + type: 'end', + label: 'Refused: duplicate', + config: { + outcome: 'refused', // 'completed' (the default) | 'refused' + message: 'Refused: {record.name} is already registered.', + }, +} +``` + +拒否された実行は失敗した実行ではありません。何も例外を投げていないからです。実行は `refused` ステータスで終わり、トリガーのルートはそのステータスとともに `200` で応答し、レンダリングされた `message` は `refusalMessage` として返され、その横に `successMessage` はありません。`outcome` が `refused` のときは `message` が必須で、完了の end では拒否されます。そこでは誰の目にも触れないからです。拒否の `end` には `decision` の分岐から到達させ、すべての書き込みは拒否が通らない分岐に置いてください。 + +## 条件と分岐 [#conditions-and-branches] + +```ts +export const expenseRouting = defineFlow({ + name: 'expense_routing', + label: 'Route new expenses', + type: 'record_change', + status: 'active', + nodes: [ + { + id: 'start', + type: 'start', + label: 'Expense submitted', + config: { objectName: 'expense', triggerType: 'record-after-create' }, + }, + { id: 'check_amount', type: 'decision', label: 'Over 10,000?' }, + { + id: 'ask_approver', + type: 'notify', + label: 'Ask the approver', + config: { recipients: '{record.approver}', title: 'Expense over 10,000: {record.name}' }, + }, + { + id: 'auto_approve', + type: 'update_record', + label: 'Auto-approve', + config: { objectName: 'expense', filter: { id: '{record.id}' }, fields: { status: 'auto_approved' } }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'check_amount' }, + { id: 'e_large', source: 'check_amount', target: 'ask_approver', condition: 'record.amount > 10000' }, + { id: 'e_other', source: 'check_amount', target: 'auto_approve', isDefault: true }, + { id: 'e2', source: 'ask_approver', target: 'end' }, + { id: 'e3', source: 'auto_approve', target: 'end' }, + ], +}); +``` + +`decision` ノードは出力エッジで分岐します。エッジは `edges` に並んだ順に評価され、`condition` が成り立つ最初のものが実行され、どれも成り立たないときは `isDefault` のエッジが実行されます。`isDefault` がない場合、条件のないエッジは、一致した分岐と並んで毎回実行されます。条件が成り立つ*すべての*分岐を実行するには、decision に `config: { mode: 'inclusive' }` を宣言します。最初の一致を採用するのは ObjectStack 17.5 からのルールです。それ以前は真になったすべての分岐が実行されていました。データベースに保存された decision(Studio から保存されたもの)は、書き換えられることなく、アップグレード時に新しい意味になりました。 + +## エラー処理 [#error-handling] + +```ts +// Keys of a record_change flow on `invoice`. The nodes are omitted: `charge_card` is an +// `http` node, `mark_paid` an `update_record`, and `flag_for_review` a `notify`. +edges: [ + { id: 'e_ok', source: 'charge_card', target: 'mark_paid' }, + { id: 'e_fault', source: 'charge_card', target: 'flag_for_review', type: 'fault' }, +], +errorHandling: { strategy: 'retry', maxRetries: 3, backoffMs: 1000, backoffMultiplier: 2 }, +``` + +どちらの仕組みもなければ、ノードの失敗で実行は終了します。これらは 2 つの範囲で働きます。 + +| | 範囲 | 失敗時 | +|---|---|---| +| `fault` エッジ | 1 つのノード | 実行はハンドラーから続行し、完了します | +| `errorHandling: { strategy: 'retry', maxRetries }` | フロー全体 | フローは最初から再実行されます。最大で `maxRetries` 回まで | + +`strategy` は `'fail'`(デフォルト)、`'retry'`、`'continue'` のいずれかです。再試行は、すでに成功したすべてのノードを再生します。2 通目のメール、2 件目の作成レコードが生まれるので、失敗が局所的な場合は fault エッジを優先してください。fault エッジが処理した失敗はフローの失敗ではなく、再試行を消費せず、失敗したステップは実行のトレースに残ります。 + +## 数式と式(CEL) [#formulas-and-expressions-cel] + +条件(開始ノードの `condition`、エッジの `condition`)は **CEL**(Common Expression Language)で書きます。安全な式評価のための Google の言語です。 + +```ts +'record.amount > 10000 && record.tier == "enterprise"' +'record.stage != previous.stage' +'!isBlank(record.notes)' +``` + +`record` と `previous` は宣言されたすべてのフィールドを持ち、値のないフィールドは `null` として読まれます。そのため `has(record.notes)` が示すのはフィールドが宣言されていることで、値があることではありません。値の有無は `record.notes != null` または `isBlank(record.notes)` で確かめてください。 + +CEL はサンドボックス化されており(副作用なし、I/O なし)、サーバー側で評価され、フロービルダーで監査できます。 + +## ビジュアルビルダー [#visual-builder] + +ObjectOS には、宣言的なメタデータと相互に変換できるビジュアルフロービルダーが付属しています。エンジニアでなくてもフローを編集でき、編集結果は手で書く TypeScript と同じ形にシリアライズされます。これは Studio の **Automations** の柱です。 + +Studio から保存したフローは、インストール済みのパッケージが提供するフローと並んで、デプロイメントに保存されます。そこから 2 つのルールが導かれます。どちらも ObjectStack 17.6 からです。 + +- **パッケージ化されたフローがその名前を取ります。** パッケージ化されたフローと同じ名前を持つ保存済みのフローは隠され、実行されません。起動ログに警告が出ます。パッケージ化されたフローをカスタマイズするには、新しい名前で複製してください。 +- **保存済みのフローは `status` で切り替えます。** オン/オフのトグルが切り替えるのはパッケージ化されたフローだけです。デプロイメント内で作成したフローは、`status: 'obsolete'` で保存するとオフになり、`status: 'active'` で保存するとオンに戻ります。 + +## フローのテスト [#testing-flows] + +```bash +os test qa/ticket-email.test.json # run a Quality Protocol test suite against a running server +``` + +`os test` は、JSON のテストスイートとして書かれた Quality Protocol のシナリオを、実行中のサーバーに対して実行します。`--tags` はタグでシナリオを選びます。[CLI リファレンス](/docs/reference/cli)を参照してください。 + +## 制限とベストプラクティス [#limits--best-practices] + +- **before フックは小さく保ちます。** 書き込みトランザクションをブロックするからです。 +- **長時間実行のステップの代わりに `wait` ノードを使います。** 実行を永続的に待機させ、タイマーまたは名前付きシグナルで再開します。 +- **独立した作業には `parallel` ブロックを使います。** 分岐を並行して実行し、フローが続行する前に合流させます。 +- **冪等性が重要です。** フローレベルの再試行は、すでに成功したノードを再実行します。外部への副作用は重複を排除すべきです(フローの実行 ID をキーとして使います)。 +- **監査上重要なアクション。** 権限を変更したりレコードを削除したりするフローは、それ自体が `sys_audit_log` にログを書くべきです。 + +## 次に進む [#where-to-go-next] + +- [Webhook](/docs/configure/webhooks):外向きの通知。フローから起動されることが多いもの +- [メール](/docs/configure/email):`notify` ノードの `email` チャネルの背後にあるトランスポート +- [通知](/docs/configure/notifications):受信者、チャネル、テンプレートがどう解決されるか +- [API アクセス](/docs/configure/api-access):外部システムからフローを呼び出す +- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation):実行エンジンのソース diff --git a/content/docs/build/automation/index.ja.mdx b/content/docs/build/automation/index.ja.mdx new file mode 100644 index 00000000..e9db7348 --- /dev/null +++ b/content/docs/build/automation/index.ja.mdx @@ -0,0 +1,43 @@ +--- +title: 自動化 +seoTitle: "自動化:フロー、ワークフロー、承認" +description: 用途に合ったツールを選びます。手順にはフロー、状態にはワークフロー、人による承認には承認を使います。 +editions: [community, cloud, enterprise] +translation: + source_sha: e39ead174c229520946d302d63cc2c7e6903c12d87048df02a15aed0b5ab0a84 + guide_rev: 1 + mode: auto +--- + +**自動化は、ビジネスロジックをアプリケーションコードに散らばらせるのではなく、ランタイムが実行するメタデータとして、データモデルに宣言的に結び付けます。** 3 つのツールがその範囲をカバーしており、最初に正しいものを選べば、後で作り直す手間が省けます。 + +## どれが必要か [#which-one-do-i-need] + +| 必要なもの | 使うもの | 読むもの | +|---|---|---| +| 「X が起きたら Y をする」:レコードの変更、スケジュール、ボタンのクリックで実行される手順 | **フロー** | [フロー](/docs/build/automation/flows) | +| 「このレコードはこれらの状態だけを通って進める」:管理されたライフサイクル | **ワークフロー**(ステートマシン) | [ワークフロー](/docs/build/automation/workflows) | +| 「先に進む前に人が承認しなければならない」:ルーティングされる人の判断 | **承認** | [承認](/docs/build/automation/approvals) | + +目安:**状態**はワークフローで、**手順**はフローでモデル化します。承認は別のエンジンではありません。承認とは、人が判断するまで承認ノードで一時停止するフローです。 + +## どう組み合わせるか [#how-they-combine] + +3 つは競合せず、組み合わせて使います。 + +- **ワークフロー**は、そもそもどのライフサイクル遷移が有効かを制約します。状態とその間の移動だけで、それ以外は扱いません。 +- **フロー**は、それらの遷移の前後で副作用を実行します。メールを送る、レコードを更新する、外部サービスを呼び出す、待機する、分岐する、などです。 +- フロー内の**承認ノード**は、承認者が操作するまで実行を止め、その後、承認または却下の分岐に沿って再開します。 + +つまり「ケースが new → assigned → resolved と進む」はワークフローであり、「エスカレーションされたらマネージャーに通知する」はフローであり、「5 万ドルを超える値引きには財務マネージャーの承認が必要」は承認ノードを持つフローです。 + +> **ヒント:** 要求が「X が起きたら Y をする」と読めるならフローです。「このレコードは決して状態を飛ばしてはならない」と読めるならワークフローです。ここから始めれば、選択を誤ることはほとんどありません。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|---|---| +| [フロー](/docs/build/automation/flows) | フローの完全なリファレンス:トリガー、ステップの種類、エラー処理、CEL | +| [ワークフロー](/docs/build/automation/workflows) | 厳密なライフサイクルのための、状態とその許可された遷移 | +| [承認](/docs/build/automation/approvals) | 承認ノード、実行アイデンティティ、承認者の体験 | +| [CEL 式](/docs/reference/cel) | 条件と検証ルールで使われる式言語 | diff --git a/content/docs/build/automation/workflows.ja.mdx b/content/docs/build/automation/workflows.ja.mdx new file mode 100644 index 00000000..00fbe406 --- /dev/null +++ b/content/docs/build/automation/workflows.ja.mdx @@ -0,0 +1,181 @@ +--- +title: ワークフロー +seoTitle: "ワークフロー:ステートマシンとしてのレコードのライフサイクル" +description: レコードのライフサイクルをステートマシンとしてモデル化します。レコードが取りうる状態、その間で許される移動、そしてフローの方がうまくできることは何も含めません。 +editions: [community, cloud, enterprise] +translation: + source_sha: 7ec55c7f19bcf4f40b34dc0d852ea4cf3534169ad263dbdff0f1ed34fbb0c600 + guide_rev: 1 + mode: auto +--- + +**ワークフローは、レコードのライフサイクルを有限ステートマシンとしてモデル化します。レコードが取りうる状態と、その間で許される移動です。** オブジェクト上の `state_machine` 検証ルールとして宣言し、状態フィールドへのすべての書き込みがそれに照らしてチェックされます。中心となる要件が「このオブジェクトはこれらの状態だけを通って進める」であるときに使います。 + +Salesforce 風の独立した Workflow Rule という作成タイプはありません。かつての「ワークフロー」の概念は、きれいに 2 つに分かれます。 + +- **`state_machine` 検証ルール**:厳密なライフサイクル遷移のため。このページです。 +- **[フロー](/docs/build/automation/flows)**:イベントで起動する、またはスケジュールされた自動化のため。[承認](/docs/build/automation/approvals)による一時停止も含みます。 + +## ワークフローとフロー [#workflows-vs-flows] + +| | ワークフロー(ステートマシン) | フロー | +|---|---|---| +| モデル化するもの | *状態*:レコードがライフサイクルのどこにあるか | *手順*:何かが起きたときに何が起こるか | +| 答える問い | 「この遷移は今許されるか?」 | 「それについて何をするか?」 | +| 形 | 状態フィールドと、許される移動の表 | ノードとエッジ:トリガー、アクション、分岐 | +| 副作用 | なし。制約するだけです | すべて:メール、更新、HTTP、待機 | + +両者は組み合わせて使います。ステートマシンが遷移を制約し、通知、レコード更新、外部呼び出しが必要なときには、フローがその前後で副作用を実行します。 + +## ステートマシンを定義する [#define-a-state-machine] + +`new → assigned → resolved` と進まなければならず、エスカレーションの経路を持つサポートケースです。ステートマシンはオブジェクトの `validations` のひとつです。 + +```ts +import { P } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const SupportCase = ObjectSchema.create({ + name: 'support_case', + label: 'Support Case', + fields: { + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'New', value: 'new', default: true }, + { label: 'Assigned', value: 'assigned' }, + { label: 'Escalated', value: 'escalated' }, + { label: 'Resolved', value: 'resolved' }, + ], + }), + resolution: Field.textarea({ + label: 'Resolution', + requiredWhen: P`record.status == 'resolved'`, // no resolved case without one + }), + }, + validations: [ + { + type: 'state_machine', + name: 'case_lifecycle', + field: 'status', + message: 'This status change is not allowed.', + initialStates: ['new'], + transitions: { + new: ['assigned'], + assigned: ['resolved', 'escalated'], + escalated: ['resolved'], + resolved: [], + }, + }, + ], +}); +``` + +これを契約として読みます。ケースは `new` として作成され、`new` のケースは担当割り当てしかできません。`assigned` のケースは解決またはエスカレーションでき、エスカレーションされたケースは解決できます。`resolved` には何も列挙されていないので、それ以上は何も移動させられません。ステータスが `resolved` のときは `resolution` が必須なので、それなしでケースを解決する書き込みは拒否されます。 + +## 構成 [#anatomy] + +| キー | 宣言するもの | +|---|---| +| `type: 'state_machine'` | この検証ルールがステートマシンであること | +| `name` | ルールの識別子(snake_case) | +| `field` | ルールが管理する状態フィールド | +| `transitions` | 各状態 → 移動できる状態のマップ。レコードの更新時にチェックされます | +| `initialStates` | レコードを作成できる状態。挿入時にチェックされます | +| `message` | 拒否された書き込みが返すエラー | + +このルールは、すべての検証ルールに共通するキー `severity`、`events`、`active`、`priority` も受け付けます。 + +### 最終状態 [#final-states] + +リストが空の状態は最終状態で、そこから出るすべての移動が拒否されます。キーがまったくない状態は最終状態ではありません。そこではチェックに判断材料がなく、書き込みを通してしまうので、「最終」を意味するときは `[]` を宣言してください。 + +### 遷移の条件 [#conditions-on-a-transition] + +`state_machine` ルールには遷移ごとのガードがありません。移動を条件付きにするには、プラットフォームが強制する場所に条件を置きます。 + +- **ある状態に必要な値**:上の `resolution` のように、そのフィールドに `requiredWhen` を付けます。値なしでその状態に入る書き込みを拒否し、ルールより前から存在する行には手を付けません。 +- **それ以外すべて**:同じ `validations` 配列に、兄弟の `script` または `conditional` 検証ルール(CEL の述語)を置きます。 + +### イベントではなく書き込み [#writes-not-events] + +マシンは状態フィールドへの書き込み時にチェックされます。`status` を現在の値の下に列挙されていない値に変える更新は拒否され、`initialStates` 以外での挿入も同様です。宣言すべき名前付きイベントはありません。正当な移動の表がマシンのすべてです。 + +> **ヒント:** マシンは最小限に保ちます。状態とその間の移動だけです。「そしてメールを送る」と言いたくなった瞬間、ワークフローの領域を離れています。その副作用は、遷移に反応する[フロー](/docs/build/automation/flows)に置いてください。 + +## 副作用はフローに置く [#side-effects-belong-in-flows] + +ステートマシンは有効な遷移を記述するだけで、それ以外は何もしません。遷移で何かを*する*必要があるとき(営業に通知する、クローズ日を記録する、外部システムを呼び出す)は、マシンとレコード変更で起動するフローを組み合わせます。 + +```ts +export const dealClosedWon = defineFlow({ + name: 'deal_closed_won', + label: 'Deal closed won', + type: 'record_change', + status: 'active', + nodes: [ + { + id: 'start', + type: 'start', + label: 'Stage moved to closed won', + config: { + triggerType: 'record-after-update', + objectName: 'opportunity', + condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'", + }, + }, + { + id: 'set_closed_date', + type: 'update_record', + label: 'Set Closed Date', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { closed_date: '{TODAY()}' }, + }, + }, + { + id: 'notify_sales', + type: 'notify', + label: 'Notify Sales', + config: { recipients: '{record.owner}', title: 'Deal won: {record.name}' }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'set_closed_date' }, + { id: 'e2', source: 'set_closed_date', target: 'notify_sales' }, + { id: 'e3', source: 'notify_sales', target: 'end' }, + ], +}); +``` + +マシンは商談が正当に `closed_won` へ*到達した*ことを保証し、フローはその後に起こることを処理します。上のような条件は CEL 式です。[CEL](/docs/reference/cel) を参照してください。 + +## ワークフロールールからの移行 [#migrating-from-workflow-rules] + +Workflow Rules を持つプラットフォームから来た場合の対応は次のとおりです。 + +| 旧来の概念 | 現在の対応するもの | +|---|---| +| Workflow Rule | フロー | +| 時間トリガー(「日付の N 日前/後」) | 相対時間トリガー:フローの開始ノードの `config.timeRelative`(16.0、[フロー](/docs/build/automation/flows)を参照) | +| 時間トリガー(固定時刻) | スケジュールフロー | +| 項目自動更新アクション | `update_record` ノード | +| メールアラート | `notify` ノード | +| HTTP 呼び出し | `http` ノード | +| 承認プロセス | 1 つ以上の `approval` ノードを持つフロー | + +判断の基準:旧ルールが「X が起きたら Y をする」なら、小さなフローとしてモデル化します。「日付フィールドの N 日前/後に Y をする」なら、相対時間トリガーを宣言します。レコード変更フローに日付が等しいという条件を付けてはいけません。それはレコードがたまたま変更されたときにしか評価されないからです。「このレコードは管理された状態を通って進まなければならない」なら、`state_machine` 検証ルールを宣言し、副作用にはフローを使います。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|---|---| +| [フロー](/docs/build/automation/flows) | 遷移の前後の副作用:トリガー、ステップ、エラー処理 | +| [承認](/docs/build/automation/approvals) | フロー内の一時停止としての、人による承認 | +| [検証ルール](/docs/build/data/validation-rules) | ステートマシンが属するルールの系統 | +| [CEL 式](/docs/reference/cel) | `requiredWhen`、検証ルール、フロー条件の背後にある言語 | +| [データモデリング](/docs/build/data) | ライフサイクルを制約するオブジェクト | +| [自動化の概要](/docs/build/automation) | 選び方:フロー、ワークフロー、承認 | diff --git a/content/docs/build/interface/actions.ja.mdx b/content/docs/build/interface/actions.ja.mdx new file mode 100644 index 00000000..c89a8f0a --- /dev/null +++ b/content/docs/build/interface/actions.ja.mdx @@ -0,0 +1,208 @@ +--- +title: アクション +seoTitle: "アクション:REST エンドポイント、ボタン、AI ツール" +description: プラットフォームが 1 つの宣言から REST エンドポイント、ボタン、AI ツールとして公開する、名前付きの操作。 +editions: [community, cloud, enterprise] +translation: + source_sha: 85e291bd4a1a724fe1ba2fbbf181d61ab30004d9389a19eaee2e9ae3175b1096 + guide_rev: 1 + mode: auto +--- + +**アクション**は、オブジェクトに対する名前付きの操作です。一度宣言すると、次のものとして現れます。 + +- `/api/v1/actions//` の **REST エンドポイント** +- レコード詳細ページの**ボタン** +- エージェントと AI Builder 向けの **AI ツール**(`action_`)。AI に公開したとき(`ai.exposed`、[下記](#from-an-ai-agent))に限ります + +3 つの画面で同じことを繰り返す必要はありません。1 つの宣言で、3 通りの呼び出し方ができます。作業が 1 ステップで終わらないときは、アクションから[フロー](/docs/build/automation/flows)を開始することもできます(`type: 'flow'`)。 + +## アクションを宣言する [#declare-an-action] + +```ts +// src/actions/approve_invoice.action.ts +import { Action } from '@objectstack/spec/ui'; + +export const approveInvoice = Action.create({ + name: 'approve_invoice', // lowercase snake_case (machine id) + label: 'Approve Invoice', + objectName: 'invoice', // attaches to the invoice object + icon: 'check', + variant: 'primary', + locations: ['record_header'], // where the button shows + description: 'Approve this invoice?', // shown in the param dialog + successMessage: 'Invoice approved', + refreshAfter: true, + + // collect input before running + params: [ + { name: 'note', label: 'Approval note', type: 'textarea' }, + ], + + // only show the button when the record is still pending + visible: 'record.status == "pending"', + + // expose it to agents and the AI Builder as `action_approve_invoice` + ai: { + exposed: true, + description: 'Approve a pending invoice, recording the approver and an optional note.', + }, + + // what it does — a sandboxed script body. `input` is the params bag; + // `ctx.record` is a read-only snapshot, so the write goes through `ctx.api`, + // which needs the `api.write` capability + type: 'script', + body: { + language: 'js', + capabilities: ['api.write'], + source: ` + await ctx.api.object('invoice').update({ + id: ctx.recordId, + status: 'approved', + approved_by: ctx.user.id, + approved_at: new Date().toISOString(), + approval_note: input.note, + }); + `, + }, +}); +``` + +`os dev` が再コンパイルした後: + +- `POST /api/v1/actions/invoice/approve_invoice` が動作します +- 請求書のレコードページに **Approve Invoice** ボタンが表示されます +- AI アシスタントは、スキルが許可していれば `action_approve_invoice` を呼び出せます。AI に公開されているからです + +## アクションの種類 [#action-types] + +`type` フィールドが、アクションが何をするかを決めます。 + +| `type` | 実行されるもの | 用途 | +|---|---|---| +| `script` | `body`:L1 の数式、またはサンドボックス化された L2 の JavaScript | ほとんどの場合。サーバー側のロジックで、監査でき、公開すれば AI から呼び出せます | +| `api` | `target` エンドポイントへの HTTP 呼び出し(`method`、`bodyExtra`) | データ API やプラットフォームのエンドポイントの再利用 | +| `flow` | `target` で名前を指定したフローを実行します | 多段階のビジネスプロセス | +| `url` | `target` の URL に移動します | ディープリンク、リダイレクト型のアクション | +| `modal` | `target` で名前を指定したページ / モーダルを開きます | カスタムのダイアログ | +| `form` | `target` で名前を指定した FormView を開きます | ガイド付きのデータ入力 | + +```ts +// api type — reuse a data-API endpoint +Action.create({ + name: 'archive_order', + objectName: 'order', + label: 'Archive', + locations: ['list_item'], + type: 'api', + method: 'PATCH', + target: '/api/v1/data/order/{id}', + bodyExtra: { archived: true }, +}); +``` + +`script` 以外の種類には `target` が必要です。どの種類でも 1 つの宣言ですが、`url`、`modal`、`form` のアクションは UI でしか実行されないので、AI ツールになることはありません。 + +## アクションを呼び出す [#calling-an-action] + +### REST [#rest] + +```bash +# the record id can go in the body, or in the path +curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ + -H 'Authorization: Bearer ' \ + -H 'Content-Type: application/json' \ + -d '{"note": "LGTM"}' +``` + +パラメータはリクエスト本文にフラットに入れて送ります。レコード ID は、末尾のパスセグメント(`.../approve_invoice/:recordId`)としても本文の中でも指定できます。レスポンスは、スクリプト本体の戻り値です(`api` の種類のアクションでは呼び出しの結果)。 + +### UI から [#from-the-ui] + +ボタンをどこに表示するかはアクション自身で宣言します。`locations`(`record_header`、`record_more`、`list_item`、`list_toolbar` など)と `order` です。その後、`visible` の述語がレコードごとにレンダリングするかどうかを決めます。リストビューでも、各行と行の選択に対するアクションを名前で指定できます。 + +```ts +// One list view; the container and `data` are omitted (see Views). +{ + type: 'grid', + columns: ['number', 'customer', 'amount', 'status'], + rowActions: ['approve_invoice', 'reject_invoice'], + bulkActions: ['send_to_customer'], +} +``` + +### フローとともに [#with-a-flow] + +アクションを呼び出すフローのノードはありません。逆向きにしてください。多段階のロジックをフローに置き、`type: 'flow'` と、`target` にフローの名前を指定したアクションからそれを開始します。 + +```ts +// flow type — start a flow by name +Action.create({ + name: 'run_invoice_approval', + objectName: 'invoice', + label: 'Run approval', + locations: ['record_more'], + type: 'flow', + target: 'approve_invoice', +}); +``` + +### AI エージェントから [#from-an-ai-agent] + +アクションがエージェントに届くのは、作成者が AI に公開したときだけです。上の例のように、UI なしで実行される種類(`script`、`flow`、`api`)で、`ai: { exposed: true }` と 40 文字以上の `ai.description` を指定します。公開されたアクションは、ObjectOS 自身のエージェントと AI Builder では `action_` ツールになり、[MCP クライアント](/docs/configure/mcp)では `run_action` になります。公開されていなければ、エージェントからは見えず、呼び出されても拒否されます。 + +`approve_invoice` が公開されていて、エージェントが持つスキルに含まれていれば、LLM はそれを呼び出せます。入力は会話から得られ、ユーザーがボタンを押した場合と同じ呼び出し時のチェックが適用されます。 + +> *「請求書 INV-2042 を、メモ『電話で確認済み』を付けて承認して。」* + +## 権限 [#permissions] + +誰がアクションを実行できるかは、呼び出されたときに、呼び出し元としてチェックされます。REST からでも、ボタンからでも、エージェントからでも同じです。 + +1. **`requiredPermissions`**:呼び出し元の[権限セット](/docs/configure/permissions/permission-sets)が持っていなければならない機能です。1 つでも欠けている呼び出し元は `403` を受け取ります。 +2. **対象のレコード**:呼び出し元として読み込まれます。呼び出し元が読めないレコードは、見つからないものとして応答されます。 +3. **AI への公開**:エージェントが届くのは、AI に公開されたアクションだけです。 +4. **UI のゲート**:`visible` と `disabled` の述語(CEL。`record`、`os.user`、パラメータに対して評価されます)が、ボタンをレンダリングするか、グレーアウトするかを制御します。 + +その後アクションが行うことは、信頼されたアプリのコードです。`script` 本体の読み書きはアプリの権限で実行され、レコードアクセスとフィールドレベルのセキュリティはそれを制限**しません**。`flow` アクションはフローの `runAs` に従います。そのため、機密性のあるものには `requiredPermissions` を宣言し、アクションを書くことはコードレビューに値する行為として扱ってください。 + +## 組み込みのアクション [#built-in-actions] + +すべてのオブジェクトには、これらが無償で用意されます。 + +| アクション | 働き | +|---|---| +| `create` | レコードを挿入します | +| `update` | レコードを更新します | +| `delete` | レコードを削除(またはソフト削除)します | +| `restore` | ソフト削除を取り消します | +| `clone` | レコードをディープコピーします | +| `share` | ユーザー、チーム、ポジション、事業単位、または単位とその配下に直接共有します | + +これらを再宣言しないでください。オブジェクトの[ライフサイクルと機能のフラグ](/docs/build/data)に従います。 + +## 監査 [#auditing] + +プラットフォームのイベントは、変更不可能な証跡である `sys_audit_log` に記録されます。次のようなフィールドがあります。 + +- `user_id`:操作を起こしたユーザー +- `action`:アクションの名前 +- `object_name` と `record_id`:何に触れたか +- `old_value` / `new_value`:変更内容 +- `ip_address` / `user_agent`:リクエストの発信元 +- `created_at`:いつ起きたか + +*「誰がボタンを押したのか?」* という質問には、まずここを見てください。 + +## AI Builder でアクションを生成する [#generating-actions-with-the-ai-builder] + +> *「`support_ticket` に `escalate_ticket` というアクションを作って。優先度を urgent に設定し、オンコールのエンジニアに割り当てるもの。」* + +[AI Builder](/docs/build/ai-builder) はアクションのメタデータを生成し、変更を承認待ちのキューに入れます。承認後、アクションは REST、UI、そして再帰的に AI 自身から呼び出せるようになります。 + +## 次に進む [#where-to-go-next] + +- [フロー](/docs/build/automation/flows):`type: 'flow'` のアクションが開始する多段階のロジック +- [エージェント](/docs/build/agents):アクションを AI ツールとして公開する +- [API アクセス](/docs/configure/api-access):外部システムからアクションを呼び出す +- [権限](/docs/configure/permissions):誰が何を呼び出せるかをゲートする diff --git a/content/docs/build/interface/apps.ja.mdx b/content/docs/build/interface/apps.ja.mdx new file mode 100644 index 00000000..d11da2d9 --- /dev/null +++ b/content/docs/build/interface/apps.ja.mdx @@ -0,0 +1,137 @@ +--- +title: アプリ +seoTitle: "アプリ:ナビゲーション、ブランディング、対象者のゲート" +description: オブジェクト、ビュー、ページ、ダッシュボードを、ブランド付きでナビゲーションできる外枠にまとめ、誰が何を見るかを正確にゲートします。 +editions: [community, cloud, enterprise] +translation: + source_sha: 92a61a2c5ecd3b085a106339e991600fa0fc1dc9db0194a67c257c4946b4f080 + guide_rev: 1 + mode: auto +--- + +**アプリ**は、オブジェクト、ビュー、ページ、ダッシュボードをひとつのまとまった体験にまとめる論理的なコンテナです。ナビゲーションツリー、ブランディング、そして決定的に重要な、誰が入れるかを定義します。 + +```ts +import { App } from '@objectstack/spec/ui' + +export const CrmApp = App.create({ + name: 'crm_app', + label: 'CRM', + icon: 'briefcase', + branding: { primaryColor: '#2563EB' }, + navigation: [ + { id: 'group_sales', type: 'group', label: 'Sales', icon: 'briefcase', children: [ + { id: 'nav_leads', type: 'object', objectName: 'crm_lead', label: 'Leads', icon: 'funnel' }, + { id: 'nav_accounts', type: 'object', objectName: 'crm_account', label: 'Accounts', icon: 'building' }, + ]}, + ], + requiredPermissions: ['crm_access'], +}) +``` + +## アプリのプロパティ [#app-properties] + +| プロパティ | 型 | 必須 | 説明 | +|:--|:--|:--|:--| +| `name` | `string` | はい | マシン名(`snake_case`) | +| `label` | `string` | はい | 表示名 | +| `icon` | `string` | — | アプリのアイコン(Lucide) | +| `description` / `version` | `string` | — | 一覧表示用のメタデータ | +| `active` | `boolean` | — | アプリが有効かどうか(デフォルト `true`) | +| `isDefault` | `boolean` | — | これがデフォルトのアプリかどうか | +| `navigation` | `NavigationItem[]` | — | ナビゲーションツリー | +| `branding` | `AppBranding` | — | `primaryColor`、`logo`、`favicon` | +| `requiredPermissions` | `string[]` | — | そもそも誰がアプリを開けるか | +| `homePageId` | `string` | — | ランディングページとして使うナビゲーション項目の `id` | + +## ナビゲーション項目 [#navigation-items] + +ナビゲーションツリーは 8 種類の項目をサポートします。よく使う 5 つは `object`、`dashboard`、`page`、`url`、`group` で、仕様ではほかに `report`、`action`、`component` の項目も定義されています。 + +| `type` | 着地先 | 主な設定 | +|:--|:--|:--| +| `object` | オブジェクトのリストビュー | `objectName`、および省略可能なターゲット指定(下記) | +| `dashboard` | ダッシュボード | `dashboardName` | +| `page` | カスタムページ | `pageName`、省略可能な `params` | +| `url` | 外部の URL | `url`、`target: '_blank'` | +| `group` | 折りたたみ可能なセクション | `children`、`expanded` | + +すべての項目は一意な `id`(小文字の `snake_case`)を宣言**しなければなりません**。`homePageId` が参照するのはこれです。共通のプロパティ:`label`、`icon`、`order`、`badge`、そして下記の 3 つのゲートです。 + +### オブジェクトのエントリのターゲット指定 [#targeting-an-object-entry] + +3 つの省略可能なフィールドで `object` エントリの着地先を絞り込めます。優先順位は `recordId` → `filters` → `viewName` です。 + +- `viewName`:エントリを名前付きのリストビューに固定します。 +- `recordId`:単一のレコードへのディープリンク(「マイプロフィール」)。`{current_user_id}` / `{current_org_id}` のテンプレート変数をサポートします。 +- `filters`:一回限りのパラメータ付きの切り出しです。エントリは素のデータ画面に着地し、各条件は削除できる URL のフィルターチップになります。ビューを作成せずに「自分に割り当てられたもの」のリンクを作るのに最適です。セキュリティ機能ではありません。何を表示するかは、引き続き行レベルの権限が決めます。 + +```ts +// One navigation item of an app. +{ id: 'nav_my_open', type: 'object', label: 'My Open Deals', objectName: 'opportunity', + filters: { owner_id: '{current_user_id}', status: 'open' }, icon: 'user-check' } +``` + +### モバイルのナビゲーション [#mobile-navigation] + +モバイル専用のナビゲーションブロックはありません。`mobileNavigation` は、どのレンダラーも読み取っていなかったため、ObjectStack 17.0 で削除されました。 + +## 対象者によるアプリのゲート [#gating-apps-by-audience] + +同じデータが、まったく異なる 2 つの対象者に役立ちます。スキーマを設計する**ビルダー**と、データを入力して読むだけの**エンドユーザー**です。これらの画面はデフォルトで分けてください。各管理者が手で隠すことに頼ってはいけません。 + +| 対象者 | 画面 | ゲートするもの | +|:--|:--|:--| +| エンドユーザー(利用者) | 厳選されたアプリ → `page` / `view` のエントリ | `App.requiredPermissions`、ナビゲーション項目のゲート | +| ビルダー / 管理者 | Setup / Studio、素のオブジェクトテーブル | 機能:`setup.access`、`studio.access`、`manage_metadata` | + +組み込みの権限セットはすでにこの区分を表しています。`member_default` と `viewer_readonly` は `studio.access` / `manage_metadata` を持た**ない**ので、これらの権限セットのユーザーにはビルダー向けの画面が見えません。`admin_full_access` と `organization_admin` は持っています。 + +各ナビゲーション項目は、独立した 3 つのゲートをサポートします。 + +| ゲート | 型 | 次の場合を除き項目を隠す…… | +|:--|:--|:--| +| `requiredPermissions` | `string[]` | ユーザーが RBAC の機能(例:`manage_metadata`)を持っている | +| `visible` | CEL 式 | 述語が真と評価される(例:`'org_admin' in current_user.positions`) | +| `requiresObject` / `requiresService` | `string` | 指定したオブジェクト / カーネルサービスがインストールされている | + +```ts +// Keys of an app; its other keys are omitted. +navigation: [ + { id: 'nav_contacts', type: 'object', label: 'Contacts', objectName: 'showcase_contact' }, + // Builder-only entry — consumers never render it: + { id: 'nav_designer', type: 'component', label: 'Object Designer', + componentRef: 'metadata:resource', params: { type: 'object' }, + requiredPermissions: ['manage_metadata'] }, +] +``` + +> **無効化せず、隠します。** 無効化されていても見えているビルダー向けのエントリは、やはりノイズです。ゲートされたナビゲーション項目は、その機能を持たないユーザーには*レンダリングされません*。エンドユーザーを混乱させるグレーアウトした部品はありません。 + +利用者向けの画面をすっきり保つ習慣がさらに 2 つあります。 + +- **エンドユーザーには素のテーブルではなくページを渡します。** `page` エントリなら、公開するものを正確に厳選できます。選んだ列、固定された可視化、有効にしたフィルターとアクションだけです。`object` エントリは制限の緩いグリッドを与えます。切り替え可能なビュー、個人用ビュー、完全なツールバーです。 +- **データ画面そのものを厳選します。** 素のオブジェクトの読み取り権限を与えて 40 列のグリッドにユーザーを放り込むよりも、[厳選したビュー](/docs/build/interface/views)を優先してください。 + +アクションのゲートは両面です。UI がボタンを隠すか無効にし、**かつ**サーバーが呼び出しを拒否します。「UI ではゲートされているがサーバーでは開いている」という落とし穴はありません。[アクション](/docs/build/interface/actions)を参照してください。 + +## Setup アプリ:組み込みの例 [#the-setup-app--a-built-in-example] + +プラットフォーム自身の管理 UI である **Setup アプリ**も、同じアプリのメタデータプロトコルからレンダリングされています。ナビゲーション、ページ、ゲートはデータとして宣言され、あなたのアプリと同じレンダラーが描画します。そのゲートは、あなたのビルダー向け画面がそうあるべきとおりのものです。利用者向けの権限セットが持たない `setup.access` 機能の背後にあります。メタデータとしてのアプリがスケールする証拠が欲しいなら、すでにそれを使っています。 + +## アンチパターン [#anti-patterns] + +- **すべてのエンドユーザーをビルダー並みの共同作業者にして**、タブを 1 つずつ隠すこと。利用者とビルダーの区分をデフォルトにしてください。 +- **ビルダー向けのエントリを隠さずに無効化すること。** 見えているのに動かない部品は、やはり混乱を招きます。 +- 厳選したページでエンドユーザーに必要なものを正確に公開できるのに、**素のオブジェクトの読み取り権限を与えること**。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|:--|:--| +| [ビュー](/docs/build/interface/views) | `object` のナビゲーションエントリの着地先 | +| [ページ](/docs/build/interface/pages) | エンドユーザー向けの厳選された `page` エントリ | +| [ダッシュボード](/docs/build/interface/dashboards) | `dashboard` エントリがレンダリングするもの | +| [アクション](/docs/build/interface/actions) | ボタンとその両面のゲート | +| [権限](/docs/configure/permissions) | `requiredPermissions` の背後にある権限セット | +| [インターフェースの概要](/docs/build/interface) | すべての部品がどう組み合わさるか | diff --git a/content/docs/build/interface/dashboards.ja.mdx b/content/docs/build/interface/dashboards.ja.mdx new file mode 100644 index 00000000..04155fc1 --- /dev/null +++ b/content/docs/build/interface/dashboards.ja.mdx @@ -0,0 +1,201 @@ +--- +title: ダッシュボード +seoTitle: "ダッシュボードの構築:データセット、ウィジェット、ドリルダウン" +description: 名前付きデータセットにバインドされたチャートウィジェットで構築する分析ページ。グローバルフィルター、自動更新、元のレコードへのドリルスルーを備えます。 +editions: [community, cloud, enterprise] +translation: + source_sha: c67b6877ef40bb832312c2b13597c8aa27a0351ad3ce7bd72469323bdef0ddc6 + guide_rev: 1 + mode: auto +--- + +**ダッシュボードはウィジェットのグリッドであり、すべてのウィジェットはデータセットにバインドします。** データセットはセマンティックレイヤーで、基になるオブジェクト、結合、ディメンション、メジャーを持ちます。ウィジェットはそれらを名前で選ぶので、「売上」は、それを使うすべてのダッシュボードとレポートで同じ意味になります。 + +```ts +// One dashboard, bound to the `sales` dataset defined below. +const salesDashboard = { + name: 'sales_overview', + label: 'Sales Overview', + description: 'Key sales metrics and pipeline analysis', + refreshIntervalSeconds: 300, // auto-refresh every 5 minutes + + dateRange: { + field: 'close_date', + defaultRange: 'this_quarter', + allowCustomRange: true, + }, + + widgets: [ + { id: 'total_revenue', title: 'Total Revenue', type: 'metric', + dataset: 'sales', values: ['revenue'], + layout: { x: 0, y: 0, w: 3, h: 2 } }, + { id: 'revenue_by_region', title: 'Revenue by Region', type: 'bar', + dataset: 'sales', dimensions: ['region'], values: ['revenue'], + layout: { x: 3, y: 0, w: 6, h: 4 } }, + { id: 'deals_by_month', title: 'Deals by Month', type: 'pie', + dataset: 'sales', dimensions: ['close_month'], values: ['deal_count'], + layout: { x: 9, y: 0, w: 3, h: 4 } }, + ], +} +``` + +## ダッシュボードのプロパティ [#dashboard-properties] + +| プロパティ | 型 | 必須 | 説明 | +|:--|:--|:--|:--| +| `name` | `string` | はい | マシン名(`snake_case`) | +| `label` | `string` | はい | 表示ラベル | +| `description` | `string` | — | ダッシュボードの説明 | +| `widgets` | `DashboardWidget[]` | はい | チャートと指標のウィジェット | +| `refreshIntervalSeconds` | `number` | — | 自動更新の間隔(秒) | +| `dateRange` | `object` | — | グローバルな日付範囲のフィルター | +| `globalFilters` | `GlobalFilter[]` | — | 対話的なフィルターコントロール | + +## まずデータセット [#datasets-first] + +データセットを一度定義し、すべてのウィジェットをそれにバインドします。 + +```ts +import { defineDataset } from '@objectstack/spec/ui' + +export const salesDataset = defineDataset({ + name: 'sales', + label: 'Sales', + object: 'opportunity', + include: ['account'], + dimensions: [ + { name: 'region', field: 'account.region', type: 'string' }, + { name: 'close_month', field: 'close_date', type: 'date', dateGranularity: 'month' }, + ], + measures: [ + { name: 'revenue', field: 'amount', aggregate: 'sum' }, + { name: 'deal_count', aggregate: 'count' }, + ], +}) +``` + +集計はウィジェットではなく、データセットのメジャー(`aggregate`)に置かれます:`count`、`sum`、`avg`、`min`、`max`、`count_distinct`、`array_agg`、`string_agg`。ウィジェットの `dimensions` と `values` は、バインドしたデータセットで宣言された名前を参照しなければなりません。 + +実行時、データセットのクエリは分析サービスを通じて実行され、基になるオブジェクトと結合したオブジェクトに、呼び出し元の行レベルセキュリティの範囲が自動的に適用されます。ダッシュボードが、ユーザーの[権限](/docs/configure/permissions)では見られないレコードを見せることはありません。 + +> 16.0 以降、ウィジェットのスキーマは**厳格**です。宣言されていないトップレベルのキー(入力ミス、ハルシネーションによるキー、削除されたインラインクエリのキー(`object` + `categoryField` + `valueField` + `aggregate`、ピボットの `rowField` / `columnField`))は、**問題のキーを名指しするパースエラー**になり、データセットの形を示します。何もレンダリングしないウィジェットへと黙って取り除かれることは、もうありません。データセットを定義してウィジェットをそれにバインドしてください。`options` は、レンダラー固有の追加設定のための自由形式の逃げ道として残っています。 + +## ウィジェット [#widgets] + +| プロパティ | 型 | 必須 | 説明 | +|:--|:--|:--|:--| +| `id` | `string` | はい | 一意なウィジェット ID(`snake_case`) | +| `dataset` | `string` | はい | バインドするデータセットの名前 | +| `values` | `string[]` | はい | メジャーの名前(少なくとも 1 つ) | +| `dimensions` | `string[]` | — | ディメンションの名前:X / グループ / 分割 | +| `type` | `ChartType` | — | 可視化の種類(デフォルト `metric`) | +| `title` / `description` | `string` | — | 表示用のタイトルとサブタイトル | +| `filter` | `FilterCondition` | — | 表示範囲のフィルター | +| `layout` | `object` | — | グリッド上の位置(省略時は自動配置) | +| `colorVariant` | `enum` | — | KPI / カードのアクセントカラー | +| `compareTo` | `enum \| object` | — | 期間比較のウィンドウ | +| `filterBindings` | `object` | — | ウィジェットごとのグローバルフィルターの対応付け(下記) | + +### チャートの種類 [#chart-types] + +| 種類 | 最適な用途 | +|:--|:--| +| `metric` *(デフォルト)* | 単一の数値の KPI:売上、件数、割合 | +| `bar` / `horizontal-bar` / `column` | カテゴリーの比較 | +| `line` | 時間の経過による傾向 | +| `pie` / `donut` | 分布 | +| `area` | 時間の経過による量 | +| `scatter` | 相関 | +| `radar` | 多次元の比較 | +| `funnel` | コンバージョンの段階 | +| `gauge` / `solid-gauge` / `bullet` / `kpi` | 目標に対する進捗 | +| `treemap` / `sankey` | 階層の比率 / 流れ | +| `table` | 詳細なレコード:**ドリルスルー** | +| `pivot` | クロス集計:**ドリルスルー** | + +### レイアウト [#layout] + +ウィジェットは 12 列のグリッドに配置されます。 + +```ts +// Keys of a dashboard widget; its other keys are omitted. +layout: { + x: 0, // column position (0-11) + y: 0, // row position + w: 6, // width in columns (1-12) + h: 4, // height in rows +} +``` + +## ドリルダウン [#drill-down] + +`table` と `pivot` のウィジェットは**ドリルスルー**できます。集計された行やセルをクリックすると、そのグループの背後にある*元のレコード*を一覧するサイドドロワーが開きます。データセットは生のグループキーを保持しているので、ドロワーのフィルターは正確なレコードに一致します。ラベルから ID を推測することはありません。ドロワー内の任意の行をクリックすると、そのレコードの詳細が開きます。**グループ → レコード一覧 → レコード**という一連の流れです。 + +ドロワーには **"Open in list →"** という逃げ道もあり、同じドリルのフィルターで範囲を絞ったまま、のぞき見をオブジェクトの完全なリストページ(並べ替え、一括選択、エクスポート、共有できる URL)に格上げします。 + +チャートのウィジェットも同じようにドリルできます。棒、点、扇形をクリックすると、ドロワーにそのバケットのレコードが一覧されます。日付のディメンションなら、そのバケットの日付範囲ぴったりです。データセットが基になるオブジェクトを公開し、ウィジェットが少なくとも 1 つのディメンションでグループ化していれば、ドリルスルーは自動的に働き、ウィジェットごとの設定は不要です。`metric` のタイルは集計値だけを表示します。[レポート](/docs/build/reports)も同じようにドリルスルーできます。 + +## グローバルな日付範囲とフィルター [#global-date-range-and-filters] + +グローバルな時間のフィルターはすべてのウィジェットに適用されます。 + +```ts +// Keys of a dashboard; its other keys are omitted. +dateRange: { field: 'created_at', defaultRange: 'this_month', allowCustomRange: true } +``` + +プリセットの範囲:`today`、`yesterday`、`this_week`、`last_week`、`this_month`、`last_month`、`this_quarter`、`last_quarter`、`this_year`、`last_year`、`last_7_days`、`last_30_days`、`last_90_days`、`custom`。 + +対話的なグローバルフィルターも同じように働きます。 + +```ts +// Keys of a dashboard; its other keys are omitted. +globalFilters: [ + { name: 'region', field: 'region', label: 'Region', type: 'select' }, + { field: 'owner', label: 'Sales Rep', type: 'lookup' }, +] +``` + +各フィルターの `name`(デフォルトは `field`)がその安定した識別子です。ウィジェットが `filterBindings` で参照するキーであり、ウィジェットの式の中で `page.` として読めるダッシュボードレベルの変数です。`dateRange` という名前は組み込みの日付範囲のために予約されています。 + +### ウィジェットごとのフィルターのバインディング [#per-widget-filter-bindings] + +デフォルトでは、フィルターはすべてのウィジェットで自身の `field` に適用されます。ウィジェットがその概念を別のフィールドに保持している場合、またはフィルターを無視すべき場合は、`filterBindings` を宣言します。 + +```ts +// Keys of a dashboard; each widget's dataset, values and title are omitted (`/* … */`). +widgets: [ + // Default binding: dateRange → created_at, region → region. + { id: 'invoices_by_status', /* … */ }, + // This widget's fields differ — map each filter explicitly. + { id: 'accounts_signed', + filterBindings: { dateRange: 'signed_at', region: 'sales_region' }, /* … */ }, + // Opt out of a filter with `false`. + { id: 'total_invoices', filterBindings: { region: false }, /* … */ }, +] +``` + +> 優先順位:明示的な `filterBindings` のエントリ(文字列による上書き、または `false` によるオプトアウト)→ フィルターの旧来の `targetWidgets` 許可リスト → フィルター自身の `field`。 + +## ダッシュボードを表示する [#surfacing-a-dashboard] + +[アプリ](/docs/build/interface/apps)に `dashboard` のナビゲーションエントリを追加します。 + +```ts +// One navigation item of an app. +{ id: 'nav_analytics', type: 'dashboard', label: 'Analytics', + dashboardName: 'sales_overview', icon: 'bar-chart' } +``` + +あるいは [AI Builder](/docs/build/ai-builder) に説明します(*「地域別の売上と月次の商談の推移を含む営業概要ダッシュボード」*)。そして承認する前に、生成されたデータセット + ダッシュボードのメタデータを確認します。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|:--|:--| +| [レポート](/docs/build/reports) | 同じデータセットに対するグループ化レポートとクロス集計レポート | +| [アプリ](/docs/build/interface/apps) | ダッシュボードをナビゲーションに置く | +| [ビュー](/docs/build/interface/views) | 同じデータセットに対するインラインの `chart` リストビュー | +| [ページ](/docs/build/interface/pages) | ウィジェットのグリッドでは足りないときの自由形式のレイアウト | +| [データモデル](/docs/build/data) | データセットが集計するオブジェクト | +| [権限](/docs/configure/permissions) | ダッシュボードが継承する行レベルセキュリティ | diff --git a/content/docs/build/interface/forms.ja.mdx b/content/docs/build/interface/forms.ja.mdx new file mode 100644 index 00000000..3e5be655 --- /dev/null +++ b/content/docs/build/interface/forms.ja.mdx @@ -0,0 +1,175 @@ +--- +title: フォーム +seoTitle: "フォーム:作成と編集、セクション、送信後" +description: 作成フォームと編集フォームを 1 つのフラットなフィールドの集合から導き出し、ずれを生まずにフィールドをセクションにまとめ、送信後に起こることを制御します。 +editions: [community, cloud, enterprise] +translation: + source_sha: 7b91231facd6607664e7ec8baee9af1949872ed91a12abe14e7b20eb2d0c96e2 + guide_rev: 1 + mode: auto +--- + +**フォームはオブジェクトのフィールドの投影であり、2 つ目のフィールドリストではありません。** フィールドはオブジェクトで一度だけ宣言し、フォームは*どのフィールドをどこに*置くかを選びます。データの意味(型、検証、デフォルト、フィールドレベルのセキュリティ)がフォームに置かれることはないので、フォームがずれてデータについて偽ることはありません。 + +フォームビューの*種類*(`simple`、`tabbed`、`wizard`、`modal` など)と公開フォームの REST の契約は、[ビュー](/docs/build/interface/views)で扱っています。このページはレイアウトの問題、つまり作成と編集、グループ化、順序についてです。 + +## 作成フォーム ≠ 編集フォーム [#create-form--edit-form] + +新規レコードのフォームはいくつかの必須項目だけを尋ね、編集フォームはレコード全体をセクションに分けて表示するべきです。**2 つのフォームを作成しないでください。** 作成フォームのサブセットは、各フィールドにすでにある意図から*導き出せます*。 + +| フィールドの手がかり | 作成フォームへの影響 | +|:--|:--| +| `required: true` | 作成時に表示**しなければなりません** | +| `readonly` / 数式 / ロールアップ / 自動採番 / システムが記録するもの | 作成時には**決して**表示しません。設定できないからです | +| `defaultValue` がある | **省略できます**。自動で埋まるからです | +| `hidden` | デフォルトではどこにも表示しません | +| `group` | フィールドが属するセクション | +| *宣言の順序* | これがデフォルトの表示順序**です**。`field.order` はありません | + +そのため、妥当な作成フォーム(*編集可能で、必須または中核のフィールドを、宣言の順序で*)は、何も作成しなくてもオブジェクトから自然に導かれます。省略が正解です。余分なものを何も出力しなくても、完全で正しいフォームが得られます。完全な編集フォームも導き出され、各 `field.group` がセクションとして具体化されます。 + +### 作成フォームを手で整えるのは、レイアウトや流れが異なる場合だけ [#hand-shape-the-create-form-only-when-layout-or-flow-diverges] + +逃げ道は、作成のエントリポイントにバインドした名前付きのフォームビューです。これは**疎な上書き**として作成します。ほとんどはフィールド名のリストです。 + +```ts +import { defineView } from '@objectstack/spec' + +const data = { provider: 'object' as const, object: 'showcase_contact' } + +export const ContactViews = defineView({ + list: { + type: 'grid', data, + columns: [{ field: 'name' }, { field: 'email' }, { field: 'company' }, { field: 'stage' }], + // Bind the "+ Add record" entry point to the slim create form: + addRecord: { enabled: true, mode: 'form', formView: 'create' }, + }, + + // Full edit form — grouped by field.group; bare strings inherit field defs. + form: { + type: 'simple', data, + sections: [ + { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] }, + { name: 'work', label: 'Work', columns: 2, fields: ['company', 'title'] }, + { name: 'status', label: 'Status', columns: 2, fields: ['stage', 'lead_score'] }, + ], + }, + + formViews: { + // Sparse create override: only the core fields, one section. + create: { + type: 'simple', data, title: 'New contact', + sections: [ + { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] }, + ], + }, + }, +}) +``` + +バインディングは `addRecord.mode: 'form'` + `addRecord.formView: 'create'` です。`formViews.create` がなければ、作成のエントリはデフォルトを導き出します。あれば、作成時にはそれが優先されます。 + +| 本当に必要なこと | 使うもの | +|:--|:--| +| 作成時に尋ねるフィールドを減らす(必須 + いくつかの中核) | **純粋な導出**。手で書かないでください | +| 作成時のグループ化が異なるが、小さなサブセットにすぎない | たいていはやはり導出 | +| 作成がウィザード / 多段階、作成専用の文言、条件付きの表示を持つ | **`formViews.create` を手で書く** | + +> 目安:*フィールドのサブセットが異なる* → 導出する。*レイアウトや流れが異なる* → 上書きする。フィールドを減らすためだけに完全な作成フォームを書くと、2 つの成果物がずれる罠にまっすぐはまります。必須フィールドを追加し、作成フォームのことを忘れ、作成時に実行時の「必須フィールドがありません」が出る、という具合です。 + +作成のフィールドは編集のフィールドの*同じ順序とグループの*サブセットなので、「4 つのフィールドでクイック作成 → 保存 → 完全なレコードに着地」が視覚的に途切れません。導出はそれを無償で保ちます。 + +## フィールドのグループ化と順序 [#field-grouping-and-order] + +モデルはフラットなフィールドの集合です。グリッドはフラットな列を表示します。フォームにはセクションが必要です。これは矛盾ではありません。1 つのフラットな集合を、異なるレンズを通して見ているのです。 + +| レンズ | フィールドの形 | 理由 | +|:--|:--|:--| +| オブジェクトの定義 | フラット | モデルは*どんなデータが存在するか*を記述します | +| テーブル / グリッド | フラットな列 | グリッドはレコード × フィールドの行列です | +| フォーム / レコードページ | グループ化されたセクション | 1 件のレコードを読む人間には区切りが必要です | + +グループ化の概念は **2 つ**あります。区別しておいてください。 + +**1. 意味上のグループ化:`field.group`(オブジェクト上)。** フィールドの論理的な居場所です。モデルとともに移動し、自動生成されるフォームの*デフォルトの*セクション分けの元になります。 + +```ts +// Fields of an object; the object around them is omitted. +fields: { + name: Field.text({ label: 'Full name', group: 'contact' }), + email: Field.email({ label: 'Email', group: 'contact' }), + stage: Field.select({ + label: 'Stage', + group: 'status', + options: [ + { label: 'Lead', value: 'lead' }, + { label: 'Customer', value: 'customer' }, + ], + }), +} +``` + +**2. レイアウト上のグループ化:フォームの `sections`(ビュー上)。** 特定のフォームの明示的な配置です。どのフィールドを、どのセクションに、何列(`columns`)で置くか、折りたたみ可能か。`field.group` をデフォルトとして継承し、必要に応じてフォームごとに上書きします。 + +```ts +// Keys of a view container; `data` and its other keys are omitted. +form: { + type: 'simple', + sections: [ + { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] }, + { name: 'status', label: 'Status', columns: 2, fields: ['stage'] }, + ], +} +``` + +**順序は作成した順序です。** `field.order` はありません。オブジェクト上のフィールドの宣言の順序が、どこでもデフォルトの表示順序であり、`sections`(とその中のフィールド)は順序付きのリストです。 + +> **「グループ」の罠。** グリッドビューの `groupByField` は、フィールドの*値*で **レコード(行)** をグループ化します。`stage = qualified` の行がすべてまとまります。フォームのセクションは **フィールド(列)** を視覚的にグループ化します。同じ言葉で、無関係な 2 つの軸です。グリッドのグループがフォームをセクションに分けることは決してありません。 + +## 送信後に起こること [#what-happens-after-submit] + +送信後の体験を制御するには、フォームビューに `submitBehavior` を追加します。 + +```ts +// Keys of a form view; its other keys are omitted. +submitBehavior: { + kind: 'thank-you', + title: 'Thanks!', + message: 'A specialist will reach out within 24 hours.', +} +``` + +| `kind` | 動作 | +|:--|:--| +| `thank-you` *(デフォルト)* | フォームを確認パネル(`title`、`message`)に置き換えます | +| `redirect` | `delayMs`(デフォルト 0)の後に `url` へ移動します。マーケティングページに最適です | +| `continue` | 次の回答のためにフォームをリセットします。キオスク、一括入力向けです | +| `next-record` | キュー内の次のレコードに進みます(内部モード) | + +公開フォームと内部フォームのどちらも、`?prefill_=` という URL パラメータに従います。メールのリンクやキャンペーンページからフォームに値を入れておけます。事前入力は UX の近道であって、権限のすり抜けではありません。値はやはり検証と、(公開フォームの場合は)サーバー側のフィールドのホワイトリストを通ります。 + +``` +/console/f/contact-us?prefill_company=Acme&prefill_email=ada@example.com +/console/forms/quick_create?prefill_lead_source=event_booth_2026 +``` + +## レコードの詳細はフォームのバインディングではなくロールで決まる [#record-detail-is-driven-by-roles-not-a-form-binding] + +フォームビューをレコード詳細画面に固定するオブジェクトレベルのキーはありません。詳細のレンダリングは、オブジェクトの画面をまたぐ**意味上のロール**から導き出されます。`nameField`(表示名)、`highlightFields`(最も重要なフィールドの帯)、`stageField`(ライフサイクルの進捗ステッパー)、そして `fieldGroups` + `Field.group`(フォーム、モーダル、詳細ページで共有されるセクション分け)です。ロールから導き出せる以上の特注のレイアウトがレコードページに必要なときは、そのオブジェクトにカスタムの[ページ](/docs/build/interface/pages)を割り当てます。 + +## アンチパターン [#anti-patterns] + +- **手で作成した 2 つの完全なフィールドリスト**(`contact_create_form` + `contact_edit_form`)。ずれることが確実です。 +- **フィールドの型 / 検証 / 選択肢をフォームで書き直すこと。** データの意味はオブジェクトにだけ置きます。 +- **すべてのフォームでグループ化を打ち直すこと。** `field.group` を一度宣言し、本当に異なる場合だけ上書きします。 +- **フォームを満たすためにデータモデルに構造的な入れ子を加えること。** モデルはフラットのままにし、フォームがグループ化します。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|:--|:--| +| [ビュー](/docs/build/interface/views) | フォームビューの種類、公開フォームの共有、ビューの完全なスキーマ | +| [データモデル](/docs/build/data) | フィールドとその意図が宣言される場所 | +| [アクション](/docs/build/interface/actions) | フォームビューを開く `type: 'form'` のアクション | +| [ページ](/docs/build/interface/pages) | 導出されたレイアウトを超えるカスタムのレコードページ | +| [権限](/docs/configure/permissions) | フォームが従うフィールドレベルのセキュリティ | diff --git a/content/docs/build/interface/index.ja.mdx b/content/docs/build/interface/index.ja.mdx new file mode 100644 index 00000000..dc6217f8 --- /dev/null +++ b/content/docs/build/interface/index.ja.mdx @@ -0,0 +1,49 @@ +--- +title: インターフェース +seoTitle: "インターフェース:アプリ、ビュー、フォーム、ダッシュボード、ページ" +description: アプリ、ビュー、フォーム、ダッシュボード、ページ、アクション。ユーザーが目にするすべての画面をメタデータとして宣言し、ObjectOS がレンダリングします。 +editions: [community, cloud, enterprise] +translation: + source_sha: b5a75aba85c279c1de7f1e2e11254b300b51906392cfaa0a5f4e1f50a803e61c + guide_rev: 1 + mode: auto +--- + +**ユーザーが目にするものはすべてメタデータです。** インターフェースはデータとして宣言し、オブジェクトと同じライフサイクルを持ちます。バージョン管理し、パッケージで出荷し、AI Builder に生成させることができ、準拠したレンダラーならどれでも描画できます。 + +各部品は上から下へと組み合わさります。 + +1. **アプリ**は外枠です。ナビゲーション、ブランディング、対象者のゲートを持ちます。 +2. その**ナビゲーション**は、オブジェクト、ダッシュボード、ページ、URL を指します。 +3. オブジェクトのエントリは**ビュー**に着地します。グリッド、カンバン、カレンダーなどです。 +4. レコードを開いたり作成したりすると**フォーム**がレンダリングされます。セクションはフィールドのメタデータから導き出され、モードごとに上書きできます。 +5. **ダッシュボード**は同じデータを、名前付きデータセットにバインドされたチャートウィジェットに集約します。 +6. **ページ**はオブジェクトの枠から完全に抜け出します。コンポーネントとリージョンで構成する自由形式のレイアウトです。 +7. **アクション**は上記のすべてにボタンを置きます。1 つの宣言で、UI、REST、フロー、AI エージェントから呼び出せます。 + +## 概念 [#the-concepts] + +| 概念 | 宣言するもの | ページ | +|:--|:--|:--| +| アプリ | ナビゲーションの外枠、ブランディング、対象者のゲート | [アプリ](/docs/build/interface/apps) | +| ビュー | レコードのレンダリング方法:グリッド、カンバン、カレンダー、ガント…… | [ビュー](/docs/build/interface/views) | +| フォーム | 作成 / 編集のレイアウト、セクション、フィールドのグループ化 | [フォーム](/docs/build/interface/forms) | +| ダッシュボード | チャートウィジェット、グローバルフィルター、ドリルダウン | [ダッシュボード](/docs/build/interface/dashboards) | +| ページ | 自由形式のコンポーネントレイアウト。パッケージ化されたドキュメントページ | [ページ](/docs/build/interface/pages) | +| アクション | あらゆる場所にボタンとして表示される名前付きの操作 | [アクション](/docs/build/interface/actions) | + +UI はデータなので、表示するレコードと同じ[権限](/docs/configure/permissions)に従います。また [AI Builder](/docs/build/ai-builder) は、同じ型付きのインターフェースを通じて、そのどれでも生成または変更できます。欲しいインターフェースを説明し、差分を確認して、承認します。 + +> まずデータから始めてください。ビューとフォームは[オブジェクトの定義](/docs/build/data)から妥当なデフォルトを導き出すので、インターフェースのメタデータを書くのは、デフォルトが望みどおりでない箇所だけです。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|:--|:--| +| [アプリ](/docs/build/interface/apps) | オブジェクトをナビゲーションできる外枠で包む | +| [ビュー](/docs/build/interface/views) | レコードのレンダリング方法を選ぶ | +| [フォーム](/docs/build/interface/forms) | 作成と編集の体験を形作る | +| [ダッシュボード](/docs/build/interface/dashboards) | 分析ウィジェットを追加する | +| [ページ](/docs/build/interface/pages) | 自由形式のレイアウトを構成する | +| [アクション](/docs/build/interface/actions) | 操作をボタンの背後に置く | +| [データモデル](/docs/build/data) | すべての画面が読み取るオブジェクト | diff --git a/content/docs/build/interface/pages.ja.mdx b/content/docs/build/interface/pages.ja.mdx new file mode 100644 index 00000000..f46decbb --- /dev/null +++ b/content/docs/build/interface/pages.ja.mdx @@ -0,0 +1,166 @@ +--- +title: ページ +seoTitle: "自由形式のページ:リージョン、コンポーネント、ドキュメントページ" +description: リージョン、コンポーネント、ローカル状態で構成する自由形式のレイアウト。さらに、パッケージの中に同梱して出荷する Markdown のドキュメントページ。 +editions: [community, cloud, enterprise] +translation: + source_sha: 4a88c0ee1ee387d77d686cf62b3d5270e2d183f2d362cd66274326205db8a3d4 + guide_rev: 1 + mode: auto +--- + +**ページは自由形式のコンテナです。** 単一のオブジェクトにバインドされる[ビュー](/docs/build/interface/views)とは異なり、ページは複数のコンポーネントを組み合わせ、ビューを埋め込み、ローカル状態を管理します。ホーム画面、カスタムのレコードレイアウト、ユーティリティパネルなどです。 + +```ts +// One page. +const homePage = { + name: 'sales_home', + label: 'Sales Home', + type: 'home', + regions: [ + { name: 'header', width: 'full', components: [ + { type: 'metric_card', id: 'total_revenue', label: 'Total Revenue', + properties: { dataset: 'sales', values: ['revenue'], format: 'currency' } }, + ]}, + { name: 'main', width: 'large', components: [ + { type: 'list_view', id: 'recent_deals', label: 'Recent Deals', + properties: { object: 'opportunity', view: 'recent_open', limit: 10 } }, + ]}, + ], +} +``` + +## ページのプロパティ [#page-properties] + +| プロパティ | 型 | 必須 | 説明 | +|:--|:--|:--|:--| +| `name` | `string` | はい | マシン名(`snake_case`) | +| `label` | `string` | はい | 表示ラベル | +| `type` | `enum` | — | ページの種類(デフォルト `'record'`、下記を参照) | +| `object` | `string` | — | 関連するオブジェクト(`record` の種類の場合) | +| `template` | `string` | — | レイアウトテンプレートの名前(デフォルト `'default'`) | +| `regions` | `PageRegion[]` | — | コンポーネントを含むレイアウトの区画 | +| `variables` | `PageVariable[]` | — | ローカル状態の変数 | +| `isDefault` | `boolean` | — | その種類のデフォルトのページかどうか | +| `assignedProfiles` | `string[]` | — | このページにアクセスできるプロファイル | + +### ページの種類 [#page-types] + +| 種類 | 用途 | +|:--|:--| +| `record` | 1 つのオブジェクトのレコードに結び付いたカスタムの詳細ページ | +| `home` | アプリのランディング / 入口 | +| `app` | 一般的なアプリケーションのレイアウト | +| `utility` | ツール、設定、ウィザード | +| `list` | データ駆動のインターフェース画面 | + +有効なのはこの 5 つの種類だけです。レンダラーが出荷されなかった以前のロードマップ上の種類は、スキーマから削除されました。 + +## リージョン [#regions] + +リージョンはレイアウトの区画で、それぞれがコンポーネントを保持します。 + +```ts +// Keys of a page; its other keys, and the regions' components, are omitted. +regions: [ + { name: 'sidebar', width: 'small', components: [/* … */] }, + { name: 'content', width: 'large', components: [/* … */] }, +] +``` + +`width` には `'small'`、`'medium'`、`'large'`、`'full'` を指定できます。 + +## コンポーネント [#components] + +コンポーネントはリージョンの中の構成要素です。 + +```ts +// One page component; the page and region around it are omitted. +{ + type: 'chart', + id: 'revenue_chart', + label: 'Revenue Trend', + properties: { chartType: 'line', dataset: 'sales', + dimensions: ['close_month'], values: ['revenue'] }, + events: { onClick: "navigate_to('opportunity_detail', { id: $event.id })" }, + visibleWhen: "'sales_manager' in os.user.positions", +} +``` + +分析を扱うコンポーネント(`chart`、`metric_card`)は名前付きの**データセット**にバインドし、その `dimensions` + `values` を名前で選びます。これは[ダッシュボード](/docs/build/interface/dashboards)のウィジェットと同じ ADR-0021 の形であり、Studio が出力する唯一の形です。削除されたインライン分析の形(コンポーネント上の `object` + `categoryField` + `valueField` + `aggregate`)は、16.0 以降はパースエラーになり、黙って無視される何もしない設定ではありません。 + +| プロパティ | 型 | 説明 | +|:--|:--|:--| +| `type` | `string` | 標準のコンポーネントの種類、またはカスタムの文字列 | +| `id` | `string` | 一意なコンポーネントインスタンスの ID | +| `properties` | `object` | コンポーネント固有の設定 | +| `events` | `object` | イベントハンドラー(アクションの式) | +| `visibleWhen` | `string` | CEL の表示条件の述語。TRUE のときだけコンポーネントがレンダリングされます(`visibility` は非推奨のエイリアスで、今も受け付けられ、`visibleWhen` に正規化されます) | +| `style` / `className` | — | その場限りの CSS | +| `responsiveStyles` | `object` | **推奨される**スタイル指定の手段:デスクトップ優先のブレークポイントごとのスタイルマップ(`large` を基準に、`medium` / `small` / `xsmall` で上書き)。スコープ付きの CSS にコンパイルされます。`var(--space-8)` のようなデザイントークンを優先してください | + +標準の、名前空間付きのコンポーネントの種類: + +| 名前空間 | 種類 | +|:--|:--| +| 構造 | `page:header`、`page:footer`、`page:sidebar`、`page:tabs`、`page:accordion`、`page:card`、`page:section` | +| レコードのコンテキスト | `record:details`、`record:highlights`、`record:related_list`、`record:activity`、`record:chatter`、`record:path`、`record:alert`、`record:quick_actions`、`record:reference_rail`、`record:history` | +| ナビゲーション | `app:launcher`、`nav:menu`、`nav:breadcrumb` | +| ユーティリティ | `global:search`、`global:notifications`、`user:profile` | +| AI | `ai:chat_window`、`ai:suggestion` | +| 要素 | `element:text`、`element:number`、`element:image`、`element:divider`、`element:button`、`element:filter`、`element:form`、`element:record_picker`、`element:text_input` | + +コンポーネントは、`dataSource`(複数オブジェクトのページ向けの、要素ごとのオブジェクトのバインディング)、`responsive`、`aria` の設定を持つこともできます。プロジェクト固有のウィジェットには、カスタムの文字列の種類も受け付けられます。 + +## 変数 [#variables] + +ページは、コンポーネントの間で共有されるローカル状態を保持します。 + +```ts +// Keys of a page; its other keys are omitted. +variables: [ + { name: 'selected_tab', type: 'string', defaultValue: 'overview' }, + { name: 'date_range', type: 'object', defaultValue: { start: null, end: null } }, +] +``` + +`type` には `'string'`(デフォルト)、`'number'`、`'boolean'`、`'object'`、`'array'`、`'record_id'` を指定できます。 + +> `record` の種類のページは、特注のレコードレイアウトのためにサポートされている方法です。ロールから導き出される詳細ページ([フォーム](/docs/build/interface/forms)を参照)では足りないときに使います。全幅のヘッダーに `record:highlights` を、サイドバーに `record:details` と `record:activity` を、メインのリージョンに `record:related_list` のコンポーネントを配置します。 + +[アプリ](/docs/build/interface/apps)から `page` のナビゲーションエントリでページを表示するか、`homePageId` でアプリのランディングページにします。 + +## ドキュメントページ [#doc-pages] + +**ドキュメント**はパッケージのドキュメントのページです。フラットな `src/docs/` ディレクトリに置いたプレーンな Markdown ファイルで、パッケージの成果物にコンパイルされ、UI の `/docs/` でレンダリングされます。ドキュメントは、AI アシスタントがパッケージについての質問に答えるときの根拠にもなります。 + +``` +src/docs/ + crm_index.md → doc name "crm_index" + crm_user_guide.md → doc name "crm_user_guide" +``` + +ファイル名の語幹がドキュメントの `name` になります。ディレクトリによる分類も、順序のファイルもありません。フラットな配置により相互参照が安定します。リンクはパスではなく、常にベース名で解決されます。 + +| ルール | 詳細 | +|:--|:--| +| 命名 | `snake_case`。ビルドの lint は**名前空間のプレフィックス**を要求します(`user_guide` ではなく `crm_user_guide`) | +| フロントマター | 省略可能。`title` と `description` が読み取られます。タイトルはフロントマター → 最初の `#` 見出し → ドキュメント名の順に解決されます | +| 相互参照 | プレーンな相対リンク(`[overview](./crm_index.md)`)。UI では `/docs/` に書き換えられ、GitHub ではそのまま機能します。**同じパッケージ内の壊れたリンクはビルドを失敗させます** | +| Markdown | CommonMark + GFM:テーブル、ハイライト付きのフェンスコード、見出しのアンカー、GitHub のアラート(`> [!TIP]`) | +| ビルド時に拒否されるもの | **MDX / 埋め込みコンポーネント**(ドキュメントはコードではなくデータであり、信頼の境界です)と**画像の参照**(まだアセットサービスがなく、壊れた画像よりも早い失敗の方がよいため) | + +ドキュメントの解決はパッケージ単位です。インストールされた 2 つのパッケージがそれぞれ同じ素の名前のドキュメントを出荷しても共存でき、どちらも他方を上書きしません。 + +> 動的なコンテンツ(ライブのフロー図、レコードのテーブル)については、コンポーネントを埋め込もうとしないでください。URL でメタデータにリンクします。ライブのビューはプラットフォームがレンダリングし、ドキュメントはそれを指し示すだけです。 + +## 次に進む [#where-to-go-next] + +| ページ | 理由 | +|:--|:--| +| [アプリ](/docs/build/interface/apps) | ページをナビゲーションに置き、`homePageId` を設定する | +| [ビュー](/docs/build/interface/views) | ページが埋め込むオブジェクトの画面 | +| [フォーム](/docs/build/interface/forms) | ロールから導き出されるレコードレイアウトとカスタムのレコードページ | +| [ダッシュボード](/docs/build/interface/dashboards) | 自由形式の代わりの、分析に特化したレイアウト | +| [アクション](/docs/build/interface/actions) | `element:button` のターゲットと `modal` の種類のアクション | +| [AI Builder](/docs/build/ai-builder) | チャットでページのメタデータを生成する | diff --git a/content/docs/build/interface/views.ja.mdx b/content/docs/build/interface/views.ja.mdx new file mode 100644 index 00000000..cb5582a3 --- /dev/null +++ b/content/docs/build/interface/views.ja.mdx @@ -0,0 +1,294 @@ +--- +title: ビュー +seoTitle: "ビュー:リスト、カンバン、カレンダー、ガント、フォーム" +description: リスト、フォーム、カンバン、カレンダー、ガントなど。すべてのオブジェクトの画面がどう宣言されるか。 +editions: [community, cloud, enterprise] +translation: + source_sha: f7208e0810feac70de2d69b3c01e2700665308487ccc26ac2e84bd3f0357fc0d + guide_rev: 1 + mode: auto +--- + +**ビュー**は、ユーザーがレコードを見て編集するための方法です。ビューは宣言的なメタデータで、オブジェクトと同じライフサイクルを持ちます。一度宣言し、パッケージとともに出荷し、どこでもレンダリングできます。 + +2 つの種類があります。 + +- **リストビュー**:多数のレコードを可視化します(グリッド、カンバン、カレンダー……) +- **フォームビュー**:単一のレコードを表示 / 編集します(シンプル、タブ、ウィザード……) + +スキーマのソース:[`packages/spec/src/ui/view.zod.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/spec/src/ui/view.zod.ts)。 + +## 最短の宣言 [#the-shortest-possible-declaration] + +ビューを 1 つも宣言しなくても、すべてのオブジェクトには自動的にデフォルトのグリッドリストとシンプルなフォームが用意されます。それらを上書きまたは拡張するには、オブジェクトのビューを、オブジェクトの中ではなくその隣にある `defineView` コンテナで宣言します。`list` がデフォルトのリスト、`form` がデフォルトのフォームで、各ビューは `data` で自分のオブジェクトを指定します。 + +```ts +// src/ui/views/support_ticket.view.ts +import { defineView } from '@objectstack/spec'; + +const data = { provider: 'object' as const, object: 'support_ticket' }; + +export const SupportTicketViews = defineView({ + list: { + type: 'grid', + data, + columns: ['subject', 'status', 'priority', 'assignee'], + }, + form: { + type: 'simple', + data, + sections: [ + { name: 'details', label: 'Details', fields: ['subject', 'description', 'priority', 'status', 'assignee'] }, + ], + }, +}); +``` + +コンテナを、それが記述するオブジェクトの隣に、スタックに登録します。 + +```ts +// objectstack.config.ts +export default defineStack({ + // ... + views: [SupportTicketViews], +}); +``` + +複数のビューを持つオブジェクト(たとえば同じデータに対するカンバン*と*カレンダー)では、同じコンテナに名前付きのビューを追加します。ビュー切り替え用には `listViews`、名前で開くフォーム用には `formViews` です。すべてのリストビューは、種類にかかわらずトップレベルの `columns` を持ちます。 + +```ts +export const SupportTicketViews = defineView({ + list: { type: 'grid', data, columns: ['subject', 'status', 'priority', 'assignee'] }, + listViews: { + by_status: { + label: 'By status', type: 'kanban', data, columns: ['subject', 'priority'], + kanban: { groupByField: 'status', columns: ['subject', 'priority'] }, + }, + schedule: { + label: 'Schedule', type: 'calendar', data, columns: ['subject', 'due_at'], + calendar: { startDateField: 'due_at', titleField: 'subject' }, + }, + by_owner: { + label: 'By owner', type: 'grid', data, columns: ['assignee', 'subject', 'status', 'priority'], + sort: [{ field: 'assignee', order: 'asc' }], + }, + }, + formViews: { + quick: { type: 'modal', data, sections: [{ name: 'quick', label: 'Quick edit', fields: ['status', 'priority', 'assignee'] }] }, + full: { + type: 'tabbed', data, + sections: [ + { name: 'details', label: 'Details', fields: ['subject', 'description'] }, + { name: 'triage', label: 'Triage', fields: ['status', 'priority', 'assignee'] }, + ], + }, + }, +}); +``` + +コンテナとそのルールは、ObjectStack の [View metadata](https://objectstack.ai/docs/ui/views) に完全に記載されています。 + +## リストビューの種類 [#list-view-types] + +すべてのリストビューには、種類にかかわらずトップレベルの `columns` が必要です。その種類のそれ以外の設定は、種類の名前を付けた設定ブロックに入れます。 + +| `type` | レンダリングするもの | 設定ブロックとその必須キー | よく使う省略可能なキー | +|:--|:--|:--|:--| +| `grid` | データテーブル(デフォルト) | — | | +| `kanban` | 列のあるボード | `kanban: { groupByField, columns }` | `summarizeField`、`titleField` | +| `gallery` | カードの束 | `gallery`(必須キーなし) | `coverField`、`titleField` | +| `calendar` | 月 / 週 / 日 | `calendar: { startDateField }` | `endDateField`、`titleField`、`colorField` | +| `timeline` | 時系列のフィード | `timeline: { startDateField, titleField }` | `endDateField`、`groupByField` | +| `gantt` | プロジェクトのタイムライン + 依存関係 | `gantt: { startDateField, endDateField, titleField }` | `progressField`、`dependenciesField` | +| `map` | 地図上のピン | `map`(必須キーなし) | `locationField`、または `latitudeField` + `longitudeField` | +| `tree` | 自己参照の階層 | `tree`(必須キーなし) | `parentField`、`labelField` | +| `chart` | 埋め込みのチャート | `chart: { dataset, values }` | `chartType`、`dimensions` | + +以下の断片はそれぞれ単一のリストビューです。どれも上のように、`data` で自分のオブジェクトを指定して、`defineView` コンテナの `list` として、または `listViews` のエントリとして入れます。 + +### リストの共通オプション [#common-list-options] + +```ts +// One list view; the container and `data` are omitted (see above). +// P is the predicate template tag: import { P } from '@objectstack/spec' +{ + type: 'grid', + columns: ['subject','status','priority','assignee','created_at'], + + filter: [ { field: 'archived', operator: 'equals', value: false } ], + sort: [ { field: 'created_at', order: 'desc' } ], + pagination: { pageSize: 25 }, + searchableFields: ['subject','description'], + filterableFields: ['status','priority','assignee'], + + navigation: { mode: 'drawer' }, // 'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none' + selection: { type: 'multiple' }, // 'none' | 'single' | 'multiple' + + rowActions: ['close_ticket','assign_to_me'], + bulkActions: ['bulk_close','bulk_export'], + + conditionalFormatting: [ + { condition: P`record.priority == 'urgent'`, style: { background: '#fef2f2', fontWeight: '600' } } // style values are strings + ], + + exportOptions: ['csv','xlsx'], + emptyState: { title: 'No tickets yet', message: 'Create one to get started', icon: 'inbox' } +} +``` + +`filter` と `sort` は [ObjectQL](/docs/reference/objectql) にコンパイルされます。`rowActions` と `bulkActions` は名前で[アクション](./actions)を参照します。 + +### カンバン [#kanban] + +```ts +// One list view; the container and `data` are omitted (see above). +{ + type: 'kanban', + columns: ['subject', 'priority', 'assignee'], // still required at the top level + kanban: { + groupByField: 'status', // discrete field — usually a select + summarizeField: 'amount', // optional total per column + columns: ['subject', 'priority', 'assignee'] // fields shown on each card + } +} +``` + +ボードの列は `groupByField` の値から作られ、その順序と色はそのフィールドの `select` の選択肢の定義から取られます。 + +列の間でドラッグ&ドロップすると、グループ化しているフィールドを設定する更新が発行されます。権限のルールは手動での編集と同じです。 + +### カレンダー [#calendar] + +```ts +// One list view; the container and `data` are omitted (see above). +{ + type: 'calendar', + columns: ['subject', 'start_at', 'end_at', 'priority'], + calendar: { + startDateField: 'start_at', + endDateField: 'end_at', // optional — single-point if omitted + titleField: 'subject', + colorField: 'priority' // optional — colours events by value + } +} +``` + +### ガント [#gantt] + +```ts +// One list view; the container and `data` are omitted (see above). +{ + type: 'gantt', + columns: ['name', 'start_at', 'due_at', 'percent_complete'], + gantt: { + startDateField: 'start_at', + endDateField: 'due_at', + titleField: 'name', + progressField: 'percent_complete', // optional, drives the progress bar + dependenciesField: 'depends_on' // optional — multiselect lookup to same object + } +} +``` + +### ツリー [#tree] + +```ts +// One list view; the container and `data` are omitted (see above). +{ + type: 'tree', + columns: ['name'], + tree: { + parentField: 'parent_id', // self-lookup that builds the hierarchy + labelField: 'name', // shown indented in the first column + defaultExpandedDepth: 1 // optional — 0 = roots only, omit = expand all + } +} +``` + +自己参照するオブジェクト(カテゴリー、部品表、組織単位、入れ子のコメント)では、ツリーグリッドが `parentField` のポインターに従って行を入れ子にします。 + +### チャート(インライン) [#chart-inline] + +`chart` リストビューは、名前付きの**データセット**(セマンティックな分析レイヤー、ADR-0021)にバインドし、そのディメンションとメジャーを**名前で**選びます。指標を一度定義すれば、同じデータセットにバインドされたすべてのチャート、ダッシュボード、レポートの一貫性が保たれます。 + +```ts +// One list view; the container and `data` are omitted (see above). +{ + type: 'chart', + columns: ['status'], + chart: { + chartType: 'bar', // 'bar' | 'line' | 'pie' | 'area' | 'scatter' + dataset: 'tickets_by_status', // a dataset declared with defineDataset(...) + dimensions: ['status'], // dataset dimension name(s) — X / group / split + values: ['ticket_count'] // dataset measure name(s) — the value axis + } +} +``` + +インラインの `xAxisField` / `yAxisFields` / `aggregation` / `groupByField` という形は 9.0 で削除されました。今では、すべてのチャート、ダッシュボードのウィジェット、レポートがデータセットにバインドします。より高度な、オブジェクトをまたぐ分析には、専用のレポート画面を使ってください。 + +## フォームビューの種類 [#form-view-types] + +| `type` | レイアウト | +|:--|:--| +| `simple` | 単一の列またはセクション(デフォルト) | +| `tabbed` | タブ付きのセクション | +| `wizard` | ステップごとの流れ | +| `split` | マスター / 詳細の 2 ペイン | +| `drawer` | サイドパネルのフォーム | +| `modal` | ダイアログのフォーム | + +```ts +// One form view; the container and `data` are omitted (see above). +{ + type: 'tabbed', + sections: [ + { label: 'Overview', fields: ['subject','status','priority','assignee'] }, + { label: 'Customer', fields: ['customer','contact','email','phone'] }, + { label: 'Resolution', fields: ['resolution_notes','resolved_at'] } + ], + submitBehavior: { kind: 'next-record' } // 'thank-you' | 'redirect' | 'continue' | 'next-record' +} +``` + +### 公開フォーム [#public-forms] + +フォームは匿名でアクセスできるようにできます。 + +```ts +// One form view; the container and `data` are omitted (see above). +{ + type: 'simple', + sections: [ { fields: ['name','email','message'] } ], + sharing: { + enabled: true, + publicLink: 'contact-us', // slug under /forms/ + allowAnonymous: true + }, + submitBehavior: { kind: 'thank-you' } +} +``` + +これにより `GET /api/v1/forms/contact-us` と `POST /api/v1/forms/contact-us/submit` が自動的に公開されます。認証を必要としない公開フォームのルートです。[REST API → Public forms](/docs/reference/rest-api#public-forms) を参照してください。 + +## 表示条件、ARIA、テーマ [#visibility-aria-theming] + +- `visibleOn: P\`...\``:フォームの**セクション**と**フィールド**で、ユーザー / レコード / 環境に基づいてそれらを隠します(CEL の述語) +- `aria: { label, description, ... }`:スクリーンリーダー向けの ARIA 属性(リストビューとフォームビュー) +- `appearance: { showDescription, allowedVisualizations: [...] }`:リストビューで、エンドユーザーが切り替えられるリストの種類を制限します + +## チャットで構築する [#build-by-chat] + +ビューのメタデータを手で書くことは通常ありません。AI Builder にこう伝えます。 + +> *「support_ticket オブジェクトのカンバンビューを、ステータスでグループ化して追加して。カードには件名と優先度を表示して。列はステータスに応じて赤 / 黄 / 緑に色分けして。」* + +AI Builder はメタデータツールを呼び出し、差分をキューに入れ、あなたが承認するとビューが表示されます。[AI Builder](/docs/build/ai-builder) を参照してください。 + +## 関連項目 [#see-also] + +- [データモデル](/docs/build/data):ビューが読み取るオブジェクト +- [アクション](./actions):`rowActions` / `bulkActions` が参照するもの +- [ObjectQL](/docs/reference/objectql):`filter` / `sort` のコンパイル先 +- [CEL](/docs/reference/cel):`conditionalFormatting`、`visibleOn` +- [`@objectstack/spec/ui/view.zod.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/spec/src/ui/view.zod.ts):スキーマ