migrate-frontend-forms について
JsonForm や FormModel のフォームを TanStack ベースの新システムへ移行する Sentry 開発者向けガイドです。機能の対応付け、エラー変換、保存動作の変更を説明します。
主な機能
- 01旧フォーム設定を新 API に対応付け
- 02自動保存と送信動作の移行
- 03サーバーエラーをフィールドエラーに変換
- 04成功・失敗コールバックの調整
- 05表示専用情報とフォームリセットの処理
活用シーン
- 01JsonForm のフォームを新システムへ移行
- 02旧来のフォーカス時保存を置き換え
- 03バックエンドのエラー変換を移行
- 04保存メッセージとリセット動作を維持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ getsentry/ sentry --skill migrate-frontend-formsインストール後、Claude Code で/migrate-frontend-formsを入力して読み込まれたか確認します。
詳細
フォーム移行ガイド
このスキルは、Sentry のレガシーフォームシステム(JsonForm、FormModel)から、新しい TanStack ベースのシステムへのフォーム移行を支援します。
機能の対応表
| 旧システム | 新システム | 備考 |
|---|---|---|
saveOnBlur: true | AutoSaveForm | デフォルトの動作 |
confirm | confirm prop | string | ((value) => string | undefined) |
showHelpInTooltip | variant="compact" | レイアウトコンポーネントで使用 |
disabledReason | disabled="reason" | 文字列でツールチップを表示 |
extraHelp | レイアウト内の JSX | フィールドの下に <Text> をレンダリング |
getData | mutationFn | ミューテーション関数内でデータを変換 |
mapFormErrors | リクエストエラーアダプター | 通常のフォームでは明示的に指定し、自動保存では提供済み |
saveMessage | onSuccess | onSuccess コールバック内のミューテーションでトーストを表示 |
formatMessageValue | onSuccess | onSuccess コールバックでトーストの内容を制御 |
resetOnError | onError | ミューテーションの onError 内で form.reset() を呼び出す |
saveOnBlur: false | useScrapsForm | 明示的な保存ボタンを備えた通常のフォームを使用 |
| (自動) | form.reset() | ミューテーション成功後もフォームがページ上に残る場合に呼び出す |
help | hintText | レイアウトコンポーネントで使用 |
label | label | レイアウトコンポーネントで使用 |
required | required | レイアウトと Zod スキーマで使用 |
| 表示専用コンテンツ | DisplayField.Row / DisplayField.Stack | 登録された入力やフォームの状態はありません |
機能の詳細
confirm → confirm prop
旧システム:
{
name: 'require2FA',
type: 'boolean',
confirm: {
true: 'Enable 2FA for all members?',
false: 'Allow members without 2FA?',
},
isDangerous: true,
}
新システム:
<AutoSaveForm
name="require2FA"
confirm={value =>
value
? 'Enable 2FA for all members?'
: 'Allow members without 2FA?'
}
{...}
>
showHelpInTooltip → variant="compact"
旧システム:
{
name: 'field',
help: 'This is help text',
showHelpInTooltip: true,
}
新システム:
<field.Layout.Row
label="Field"
hintText="This is help text"
variant="compact"
>
disabledReason → disabled="reason"
旧システム:
{
name: 'field',
disabled: true,
disabledReason: 'Requires Business plan',
}
新システム:
<field.Input
disabled="Requires Business plan"
{...}
/>
extraHelp → JSX
旧システム:
{
name: 'sensitiveFields',
help: 'Main help text',
extraHelp: 'Note: These fields apply org-wide',
}
新システム:
<field.Layout.Stack label="Sensitive Fields" hintText="Main help text">
<field.TextArea {...} />
<Text size="sm" variant="muted">
Note: These fields apply org-wide
</Text>
</field.Layout.Stack>
getData → mutationFn
getData 関数は、API に送信する前にフィールドデータを変換していました。新しいシステムでは、mutationFn 内で処理します。
旧システム:
// Wrap field value in 'options' key
{
name: 'sentry:csp_ignored_sources_defaults',
type: 'boolean',
getData: data => ({options: data}),
}
// Or extract/transform specific fields
{
name: 'slug',
getData: (data: {slug?: string}) => ({slug: data.slug}),
}
新システム:
<AutoSaveForm
name="sentry:csp_ignored_sources_defaults"
schema={schema}
initialValue={project.options['sentry:csp_ignored_sources_defaults']}
mutationOptions={{
mutationFn: data => {
// Transform data before API call (equivalent to getData)
const transformed = {options: data};
return fetchMutation({
url: `/projects/${organization.slug}/${project.slug}/`,
method: 'PUT',
data: transformed,
});
},
}}
>
{field => (
<field.Layout.Row label="Use default ignored sources">
<field.Switch checked={field.state.value} onChange={field.handleChange} />
</field.Layout.Row>
)}
</AutoSaveForm>
より簡単なパターン - 値をラップするだけでよい場合:
mutationOptions={{
mutationFn: fieldData => {
return fetchMutation({
url: `/projects/${org}/${project}/`,
method: 'PUT',
data: {options: fieldData}, // getData equivalent
});
},
}}
重要:ミューテーションの型を正しく指定する
mutationFn の型には、スキーマから推論される型ではなく、API のデータ型(例:Partial<Organization>、Partial<Project>)を指定してください。スキーマはクライアント側のフィールド検証にのみ使用され、ミューテーションが受け取るのは API エンドポイントが受け入れるデータです。ミューテーションをスキーマに結び付けると、互いに無関係な関心事が密結合し、スキーマの型が API の型と完全には一致しない場合に型エラーが発生する可能性があります。
// ❌ Don't use generic types - breaks field type narrowing
mutationOptions={{
mutationFn: (data: Record<string, unknown>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}
// ❌ Don't tie mutation type to the zod schema
mutationOptions={{
mutationFn: (data: Partial<z.infer<typeof preferencesSchema>>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}
// ✅ Use the API's data type
mutationOptions={{
mutationFn: (data: Partial<UserDetails>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}
Zod スキーマの型が API の型と互換性がある(つまり、API の型に代入可能である)ことを確認してください。たとえば、API が 'off' | 'low' | 'high' のような文字列ユニオンを想定している場合は、z.string() ではなく z.enum(['off', 'low', 'high']) を使用してください。
useMutation、mutationOptions、または TanStack Query の関数に、呼び出し側でジェネリクスを渡してはいけません。 これはデータ、エラー、変数、コンテキストを含むすべてのジェネリクスに適用されます。型は明示的に指定せず、推論させてください。「TanStack Query Type Inference」の項にある static/AGENTS.md のルール全文を参照してください。
// ❌ Generics on useMutation — NEVER do this
const mutation = useMutation<CodeOwner, RequestError, [Payload]>({
mutationFn: ([payload]) => fetchMutation({url, method: 'POST', data: payload}),
});
// ❌ Generics on mutationOptions — NEVER do this either
mutationOptions<unknown, RequestError, Variables, MyContext>({...})
// ❌ Explicit context type — inferred from onMutate return
type MyContext = {changeId: string};
// ❌ RequestError as error generic — it's a type assertion in disguise
// Other things can go wrong that would NOT yield a RequestError
// ✅ Type the mutationFn payload; fetchMutation<T> carries the return type
const mutation = useMutation({
mutationFn: (payload: {codeMappingId: string; raw: string}) =>
fetchMutation<CodeOwner>({
url: `/projects/${org}/${project}/codeowners/`,
method: 'POST',
data: payload,
}),
});
// ✅ Context is inferred from onMutate, error is Error by default
mutationOptions({
mutationFn: (variables: MyVars) => fetchMutation<MyResponse>({...}),
onMutate: async () => {
return {changeId: uniqueId()}; // context type inferred from this
},
onError: (_error, _vars, context) => {
// context?.changeId is typed automatically
// _error is Error — use runtime narrowing for RequestError
},
})
mapFormErrors → requestErrorToFieldErrors + setFieldErrors
mapFormErrors 関数は、API エラーレスポンスをフィールド固有のエラーに変換していました。新しいシステムでは、Sentry API エラーを requestErrorToFieldErrors で変換し、その Scraps の FieldErrors 結果を setFieldErrors に渡します。
RequestError を直接 setFieldErrors に渡さないでください。Scraps は Sentry の API クライアント型に依存していません。
旧:
// Form-level error transformer
function mapMonitorFormErrors(responseJson?: any) {
if (responseJson.config === undefined) {
return responseJson;
}
// Flatten nested config errors to dot notation
const {config, ...rest} = responseJson;
const configErrors = Object.fromEntries(
Object.entries(config).map(([key, value]) => [`config.${key}`, value])
);
return {...rest, ...configErrors};
}
<Form mapFormErrors={mapMonitorFormErrors} {...}>
新:
import {setFieldErrors} from '@sentry/scraps/form';
import {RequestError} from 'sentry/utils/requestError/requestError';
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {...},
validators: {onDynamic: schema},
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
if (!(error instanceof RequestError)) {
return;
}
// Keep custom mapping only when the legacy form reshaped the response.
const responseJson = error.responseJSON;
if (responseJson?.config) {
// Flatten nested errors to dot notation
const {config, ...rest} = responseJson;
const errors: Record<string, {message: string}> = {};
for (const [key, value] of Object.entries(rest)) {
errors[key] = {message: Array.isArray(value) ? value[0] : String(value)};
}
for (const [key, value] of Object.entries(config)) {
errors[`config.${key}`] = {message: Array.isArray(value) ? value[0] : String(value)};
}
setFieldErrors(formApi, errors);
}
}
},
});
よりシンプルなパターン - フラットなエラーレスポンスの場合:
import {setFieldErrors} from '@sentry/scraps/form';
import {RequestError} from 'sentry/utils/requestError/requestError';
import {requestErrorToFieldErrors} from 'sentry/utils/requestError/requestErrorToFieldErrors';
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
if (!(error instanceof RequestError)) {
addErrorMessage(t('Unable to save changes.'));
return;
}
const handled = setFieldErrors(
formApi,
requestErrorToFieldErrors(error, formApi.state.values)
);
if (!handled) {
addErrorMessage(t('Unable to save changes.'));
}
}
},
requestErrorToFieldErrors は RequestError を受け付けます。変換前に、Sentry の呼び出し箇所で unknown エラーを絞り込んでください。アダプターはレスポンスのキーを formApi.state.values に対してフィルタリングし、Scraps のフィールドエラーの形で返します。直接 FieldErrors オブジェクトを使用するのは、上記のネストされた config の例のように、移行でレスポンスのカスタム整形が必要な場合だけにしてください。
AutoSaveForm では、標準的なリクエストエラー処理は自動的に行われます。Sentry のフォームエラープロバイダーは、フィールドエラーには requestErrorToFieldErrors を、リクエストの詳細やステータスメッセージには getRequestErrorUserMessage を使用します。通常のフォーム用の catch ブロックを各自動保存フィールドに追加しないでください。
Note:
setFieldErrorsはドット記法によるネストされたパスをサポートしています:'config.schedule': {message: 'Invalid schedule'}
saveMessage → onSuccess
saveMessage は、保存の成功後にカスタムのトースト/アラートを表示していました。新しいシステムでは、これを mutation の onSuccess コールバックで処理します。
旧:
{
name: 'fingerprintingRules',
saveOnBlur: false,
saveMessageAlertVariant: 'info',
saveMessage: t('Changing fingerprint rules will apply to future events only.'),
}
新:
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
<AutoSaveForm
name="fingerprintingRules"
schema={schema}
initialValue={project.fingerprintingRules}
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onSuccess: () => {
// Custom success message (equivalent to saveMessage)
addSuccessMessage(t('Changing fingerprint rules will apply to future events only.'));
},
}}
>
formatMessageValue → onSuccess
formatMessageValue は、変更された値が成功トーストでどのように表示されるかを制御していました。これを false に設定すると、値の表示が完全に無効化されていました (大きなテキストフィールドで便利です)。新しいシステムでは、これを onSuccess で直接制御します。
旧:
{
name: 'fingerprintingRules',
saveMessage: t('Rules updated'),
formatMessageValue: false, // Don't show the (potentially huge) value in toast
}
新:
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onSuccess: () => {
// Just show the message, no value (equivalent to formatMessageValue: false)
addSuccessMessage(t('Rules updated'));
},
}}
// Or if you want to show a formatted value:
onSuccess: (data) => {
addSuccessMessage(t('Slug changed to %s', data.slug));
},
resetOnError → onError
resetOnError オプションは、保存に失敗したときにフィールドを以前の値に戻していました。新しいシステムでは、mutation の onError コールバックで form.reset() を呼び出します。
旧:
// Form-level reset on error
<Form resetOnError apiEndpoint="/auth/" {...}>
// Or field-level (BooleanField always resets on error)
<FormField resetOnError name="enabled" {...}>
新 (useScrapsForm を使用):
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {password: ''},
validators: {onDynamic: schema},
onSubmit: async ({value}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// Reset form to previous values on error (equivalent to resetOnError)
form.reset();
throw error; // Re-throw if you want error handling to continue
}
},
});
新 (AutoSaveForm を使用):
<AutoSaveForm
name="enabled"
schema={schema}
initialValue={settings.enabled}
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onError: () => {
// The field automatically shows error state via TanStack Query
// If you need to reset the value, you can pass a reset callback
},
}}
>
Note: TanStack Query を使った AutoSaveForm は、すでにエラー状態を適切に処理します - mutation の
isError状態が UI に反映されます。手動でのリセットは、通常、パスワードフィールドのような特定の UX 要件にのみ必要です。
保存後のリセット
保存後もページに留まるフォームで useScrapsForm を使用する場合、mutation の成功後に form.reset() を呼び出してください。これにより、フォームが更新された defaultValues と再同期され、再び pristine な状態になります — フォームが dirty であることに依存する UI (条件付きで表示される保存/キャンセルボタンなど) は正しく更新されます。
onSubmit: ({value}) =>
mutation
.mutateAsync(value)
.then(() => form.reset())
.catch(() => {}),
Note:
AutoSaveFormはこれを自動的に処理します。追加が必要なのはuseScrapsFormを使用する場合だけです。
saveOnBlur: false → useScrapsForm
saveOnBlur: false のフィールドは、自動保存の代わりに保存/キャンセルボタンを備えたインラインアラートを表示していました。これは危険な操作 (slug の変更) や大きなテキストの編集 (fingerprint ルール) で使用されていました。
新しいシステムでは、useScrapsForm を使った通常のフォームと明示的な保存ボタンを使用します。これにより、コミットする前に警告を表示するという UX が維持されます。
旧:
{
name: 'slug',
type: 'string',
saveOnBlur: false,
saveMessageAlertVariant: 'warning',
saveMessage: t("Changing a project's slug can break your build scripts!"),
}
新:
import {Alert} from '@sentry/scraps/alert';
import {Button} from '@sentry/scraps/button';
import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form';
const slugSchema = z.object({
slug: z.string().min(1, 'Slug is required'),
});
function SlugForm({project}: {project: Project}) {
const mutation = useMutation({
mutationFn: (data: {slug: string}) =>
fetchMutation({url: `/projects/${org}/${project.slug}/`, method: 'PUT', data}),
});
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {slug: project.slug},
validators: {onDynamic: slugSchema},
onSubmit: ({value}) => mutation.mutateAsync(value).catch(() => {}),
});
return (
<form.AppForm form={form}>
<form.AppField name="slug">
{field => (
<field.Layout.Stack label="Project Slug">
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
{/* Warning shown before saving (equivalent to saveMessage) */}
<Alert variant="warning">
{t("Changing a project's slug can break your build scripts!")}
</Alert>
<Flex gap="sm" justify="end">
<form.ResetButton>Reset</form.ResetButton>
<form.SubmitButton>Save</form.SubmitButton>
</Flex>
</form.AppForm>
);
}
このパターンを使うべき場合:
- コミット前にユーザーが警告を確認すべき危険な操作 (slug の変更、セキュリティトークン)
- 保存する前に編集を終えたい大きな複数行のテキストフィールド (fingerprint ルール、フィルター)
- 自動保存が意味をなさないあらゆるフィールド
フォームを通して送信し、フォームを迂回しないでください。 上記の SlugForm パターンに従ってください — mutation は onSubmit で実行され、保存ボタンは <form.SubmitButton> です。onSubmit なしで <form.AppForm> をレンダリングし、独立した <Button onClick> から mutation をトリガーしないでください:
// ❌ Form is never submitted; mutation fires from a separate button
const form = useScrapsForm({
...defaultFormOptions,
defaultValues,
validators: {onDynamic: schema},
// no onSubmit
});
return (
<form.AppForm form={form}>
<form.AppField name="codeMappingId">{...}</form.AppField>
<Button onClick={() => mutation.mutate(...)}>Save</Button>
</form.AppForm>
);
実際に送信されることのないフォームは、バリデーション、pending/disabled 状態、およびフィールドエラーの配線を迂回してしまいます。
表示専用情報
日付、ステータステキスト、入力欄のないその他のコンテンツには、@sentry/scraps/form の DisplayField.Row または DisplayField.Stack を使用します。レイアウトを整えるためだけに AppField や AutoSaveForm を作成しないでください。
<DisplayField.Row label={t('Created')}>
<Text>
<DateTime date={data.dateCreated} />
</Text>
</DisplayField.Row>
このコンテンツの表示だけに使われていたスキーマエントリーとデフォルト値を削除します。必要に応じて、<Text> を使ってテキストのスタイルを維持してください。レンダリング中の派生表示値の計算には既存の form.Subscribe を使用し、form.setFieldValue でフォームの状態にコピーしないでください。
Row はレスポンシブなフィールドレイアウトに合わせて配置し、Stack は常に縦に積み重ねて配置します。これらのコンポーネントは、<label>、フィールドコンテキスト、エラーフォーカスの登録を伴わない説明リストのセマンティクスを使用します。実際の読み取り専用入力は、入力との関連付けを維持できるよう、AppField と field.Layout.* の内側にある field.Input readOnly を使用してください。フォームの状態を扱うカスタムコントロールには、引き続き field.Base が必要です。
レイアウト用の props とサブスクリプションの例については、フォーム生成ガイドを参照してください。
フォーム検索機能の維持
Sentry の SettingsSearch では、ユーザーが個々の設定フィールドを検索できます。フォームを移行する際は、移行したフォームを FormSearch でラップして、この検索性を維持する必要があります。
FormSearch コンポーネント
FormSearch はビルド時のマーカーコンポーネントです。実行時の動作はなく、子要素をそのままレンダリングするだけです。route prop は静的抽出スクリプトによって読み取られ、フォームフィールドとナビゲーションルートを関連付けることで、SettingsSearch の検索結果に表示できるようにします。
import {FormSearch} from 'sentry/components/core/form';
<FormSearch route="/settings/account/details/">
<FieldGroup title={t('Account Details')}>
<AutoSaveForm name="name" schema={schema} initialValue={user.name} mutationOptions={...}>
{field => (
<field.Layout.Row label={t('Name')} hintText={t('Your full name')} required>
<field.Input />
</field.Layout.Row>
)}
</AutoSaveForm>
</FieldGroup>
</FormSearch>
Props:
| Prop | 型 | 説明 |
|---|---|---|
route | string | このフォームの設定ルート(例:'/settings/account/details/')。検索ナビゲーションに使用します。 |
children | ReactNode | フォームのコンテンツ。実行時には変更せずにレンダリングされます。 |
ルール:
routeは設定ページの URL と完全に一致する必要があります(末尾のスラッシュを含む)。- 個々のフィールドではなく、フォームセクション全体を単一の
FormSearchでラップします。 FormSearch内のすべての<AutoSaveForm>または<form.AppField>がインデックスに登録されます。labelとhintTextは、プレーンな文字列リテラルまたはt()の呼び出しにしてください。計算された文字列や動的な文字列は抽出時にスキップされます。
フォームフィールドレジストリ
FormSearch ラッパーを追加または更新した後、検索結果を最新の状態に保つために、フィールドレジストリを再生成します。
pnpm run extract-form-fields
このスクリプト(./scripts/extractFormFields.ts)はすべての TSX ファイルをスキャンし、<FormSearch> コンポーネントを見つけて、フィールドのメタデータ(name、label、hintText、route)を抽出し、生成したレジストリを static/app/views/settings/fieldRegistry.generated.ts に書き込みます。移行 PR とともにこの生成ファイルをコミットしてください。このファイルはソースツリーの一部です。
FormSearchラッパー内のフォームを変更(追加、削除、ラベルの変更)するたびに、このコマンドを実行してください。生成ファイルはリポジトリにチェックインされるため、手動で編集しないでください。
移行:旧フォームがすでに検索可能な場合
移行対象の旧 JsonForm がすでに SettingsSearch のインデックスに登録されていた場合(つまり、sentry/data/forms にエントリーがある場合)、検索機能を維持するため、新しいフォームに FormSearch ラッパーを追加する必要があります。旧ソースと新ソースは共存します。同じルートとフィールドの組み合わせでは新しいレジストリエントリーが古いエントリーより優先されますが、旧フォームを削除すると古いエントリーはなくなります。
null の初期値の処理
従来のセレクトフィールドは、空または未定義の値で開始し、選択が必要でした。フィールドが常に必須の場合は、スキーマで .nullable().refine() を使用し、defaultValues の型に z.input<typeof schema> を指定して、onSubmit で schema.parse(value) を呼び出します。
変更前:
{
name: 'provider',
type: 'select',
required: true,
choices: [['github', 'GitHub'], ['launchdarkly', 'LaunchDarkly']],
}
変更後:
const schema = z.object({
provider: z
.enum(['github', 'launchdarkly'])
.nullable()
.refine(v => v !== null, 'Provider is required'),
});
// z.input accepts null; z.output (after refine) does not
const defaultValues: z.input<typeof schema> = {
provider: null,
};
const form = useScrapsForm({
...defaultFormOptions,
defaultValues,
validators: {onDynamic: schema},
onSubmit: ({value}) => {
// schema.parse narrows null away — mutation receives z.output
return mutation.mutateAsync(schema.parse(value)).catch(() => {});
},
});
このパターンは、必須フィールドに意味のある初期値がない場合に必要です。z.input と z.output の区別により、フォームではデフォルト値として null を受け入れつつ、ミューテーションには検証済みの null 以外の型を渡せます。
条件付き必須フィールド
別の値によって表示されている間だけ必須となる従来のフィールドを移行する場合は、フォームレベルのスキーマを寛容に保ち、レンダリングされた AppField で検証します。この条件付き要件にフォームレベルの .superRefine() を使用しないでください。 フィールドが非表示でもフォームレベルの検証は実行されるため、ユーザーがそのフィールドを確認したり修正したりできないにもかかわらず、必須エラーによって送信がブロックされる可能性があります。
const schema = z.object({
plan: z.enum(['standard', 'enterprise']),
billingEmail: z.string(),
});
<form.Subscribe selector={state => state.values.plan === 'enterprise'}>
{showBilling =>
showBilling ? (
<form.AppField
name="billingEmail"
validators={{onDynamic: z.string().trim().email('Enter a valid billing email')}}
>
{field => (
<field.Layout.Stack label="Billing Email" required>
<field.Input value={field.state.value} onChange={field.handleChange} />
</field.Layout.Stack>
)}
</form.AppField>
) : null
}
</form.Subscribe>;
null 許容の数値フィールドでは、フォームスキーマに z.number().nullable() を使用し、z.number().positive('Amount must be greater than zero').nullable().refine(value => value !== null, 'Amount is required') のようなフィールドバリデーターを使用します。フィールドバリデーターは、表示されている入力欄が空のときに出力される null を拒否します。
意図的に移行していないもの
| 機能 | 使用箇所 | 理由 |
|---|---|---|
allowUndo | 3 forms | トーストでの取り消しは、メリットがわずかな一方で複雑さが増します。代わりにシンプルなエラートーストを使用してください。 |
移行チェックリスト
- JsonForm/FormModel を useScrapsForm または AutoSaveForm に置き換える
-
useMutationにジェネリクスを指定しない —mutationFnのペイロードに型を指定し、戻り値の型にはfetchMutation<T>を使用する - Save ボタンを使う
useScrapsFormでは、ミューテーションをonSubmitで実行し、<form.SubmitButton>から呼び出す(送信されないフォームを作らない) - 入力フィールドの設定オブジェクトを JSX の AppField コンポーネントに変換する
- 表示専用のコンテンツを
DisplayField.RowまたはDisplayField.Stackに変換し、スキーマやフォーム状態のエントリーを設けない - 条件付き必須フィールドは、寛容なフォームスキーマを使い、レンダリングされた
AppFieldで検証する - レイアウトの
helpをhintTextに置き換える -
showHelpInTooltipをvariant="compact"に置き換える -
disabledReasonをdisabled="reason string"に置き換える -
extraHelpをレイアウト内の追加 JSX に置き換える -
confirmオブジェクトを関数に変換する:(value) => message | undefined -
mutationFnでgetDataを処理する - 通常のフォームでは、
requestErrorToFieldErrorsの前に不明なエラーをRequestErrorに絞り込み、その後setFieldErrorsを呼び出す -
AutoSaveFormでは、アプリから提供されるリクエストエラー処理を使用する -
mapFormErrorsが API レスポンスの形状を変更した場合に限り、カスタムエラーマッピングを維持する -
onSuccessコールバックでsaveMessageを処理する -
saveOnBlur: falseのフィールドを、Save ボタン付きの通常のフォームに変換する - ミューテーションの成功後に
form.reset()を呼び出す(ページに残るフォームの場合) -
onSuccessのキャッシュ更新で既存データとのマージを確認する(アップデーター関数を使用する) — API エンドポイントによっては部分的なオブジェクトを返す場合があります - 以前のフォームが SettingsSearch で検索可能だった場合は、移行後のフォームを
<FormSearch route="...">でラップする -
pnpm run extract-form-fieldsを実行し、更新されたfieldRegistry.generated.tsをコミットする