migrate-frontend-forms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseForm Migration Guide
表单迁移指南
This skill helps migrate forms from Sentry's legacy form system (JsonForm, FormModel) to the new TanStack-based system.
本指南帮助将表单从Sentry的旧版表单系统(JsonForm、FormModel)迁移到基于TanStack的新系统。
Feature Mapping
功能映射
| Old System | New System | Notes |
|---|---|---|
| | Default behavior |
| | |
| | On layout components |
| | String shows tooltip |
| JSX in layout | Render |
| | Transform data in mutation function |
| | Transform API errors in catch block |
| | Show toast in mutation onSuccess callback |
| | Control toast content in onSuccess callback |
| | Call form.reset() in mutation onError |
| | Use regular form with explicit Save button |
| | On layout components |
| | On layout components |
| | On layout + Zod schema |
| 旧系统 | 新系统 | 说明 |
|---|---|---|
| | 默认行为 |
| | |
| | 用于布局组件 |
| | 字符串会显示为提示框 |
| 布局中的JSX | 在字段下方渲染 |
| | 在mutation函数中转换数据 |
| | 在catch块中转换API错误 |
| | 在mutation的onSuccess回调中显示提示框 |
| | 在onSuccess回调中控制提示框内容 |
| | 在mutation的onError中调用form.reset() |
| | 使用带显式保存按钮的常规表单 |
| | 用于布局组件 |
| | 用于布局组件 |
| | 用于布局组件 + Zod schema |
Feature Details
功能详情
confirm → confirm
prop
confirmconfirm → confirm
属性
confirmOld:
tsx
{
name: 'require2FA',
type: 'boolean',
confirm: {
true: 'Enable 2FA for all members?',
false: 'Allow members without 2FA?',
},
isDangerous: true,
}New:
tsx
<AutoSaveField
name="require2FA"
confirm={value =>
value
? 'Enable 2FA for all members?'
: 'Allow members without 2FA?'
}
{...}
>旧版写法:
tsx
{
name: 'require2FA',
type: 'boolean',
confirm: {
true: 'Enable 2FA for all members?',
false: 'Allow members without 2FA?',
},
isDangerous: true,
}新版写法:
tsx
<AutoSaveField
name="require2FA"
confirm={value =>
value
? 'Enable 2FA for all members?'
: 'Allow members without 2FA?'
}
{...}
>showHelpInTooltip → variant="compact"
variant="compact"showHelpInTooltip → variant="compact"
variant="compact"Old:
tsx
{
name: 'field',
help: 'This is help text',
showHelpInTooltip: true,
}New:
tsx
<field.Layout.Row
label="Field"
hintText="This is help text"
variant="compact"
>旧版写法:
tsx
{
name: 'field',
help: 'This is help text',
showHelpInTooltip: true,
}新版写法:
tsx
<field.Layout.Row
label="Field"
hintText="This is help text"
variant="compact"
>disabledReason → disabled="reason"
disabled="reason"disabledReason → disabled="reason"
disabled="reason"Old:
tsx
{
name: 'field',
disabled: true,
disabledReason: 'Requires Business plan',
}New:
tsx
<field.Input
disabled="Requires Business plan"
{...}
/>旧版写法:
tsx
{
name: 'field',
disabled: true,
disabledReason: 'Requires Business plan',
}新版写法:
tsx
<field.Input
disabled="Requires Business plan"
{...}
/>extraHelp → JSX
extraHelp → JSX
Old:
tsx
{
name: 'sensitiveFields',
help: 'Main help text',
extraHelp: 'Note: These fields apply org-wide',
}New:
tsx
<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>旧版写法:
tsx
{
name: 'sensitiveFields',
help: 'Main help text',
extraHelp: 'Note: These fields apply org-wide',
}新版写法:
tsx
<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
mutationFngetData → mutationFn
mutationFnThe function transformed field data before sending to the API. In the new system, handle this in the .
getDatamutationFnOld:
tsx
// 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}),
}New:
tsx
<AutoSaveField
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>
)}
</AutoSaveField>Simpler pattern - If you just need to wrap the value:
tsx
mutationOptions={{
mutationFn: fieldData => {
return fetchMutation({
url: `/projects/${org}/${project}/`,
method: 'PUT',
data: {options: fieldData}, // getData equivalent
});
},
}}Important: Typing mutations with mixed-type schemas
When using with schemas that have mixed types (e.g., strings and booleans), the mutation function must be typed using the schema-inferred type. Using generic types like breaks TanStack Form's ability to narrow field types based on the field name.
AutoSaveFieldRecord<string, unknown>tsx
const preferencesSchema = z.object({
theme: z.string(),
language: z.string(),
notifications: z.boolean(),
});
type Preferences = z.infer<typeof preferencesSchema>;
// ❌ Don't use generic types - breaks field type narrowing
mutationOptions={{
mutationFn: (data: Record<string, unknown>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}
// ✅ Use schema-inferred type for proper type narrowing
mutationOptions={{
mutationFn: (data: Partial<Preferences>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}getDatamutationFn旧版写法:
tsx
// 将字段值包装在'options'键中
{
name: 'sentry:csp_ignored_sources_defaults',
type: 'boolean',
getData: data => ({options: data}),
}
// 或者提取/转换特定字段
{
name: 'slug',
getData: (data: {slug?: string}) => ({slug: data.slug}),
}新版写法:
tsx
<AutoSaveField
name="sentry:csp_ignored_sources_defaults"
schema={schema}
initialValue={project.options['sentry:csp_ignored_sources_defaults']}
mutationOptions={{
mutationFn: data => {
// API调用前转换数据(等效于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>
)}
</AutoSaveField>更简洁的写法 - 如果只需要包装值:
tsx
mutationOptions={{
mutationFn: fieldData => {
return fetchMutation({
url: `/projects/${org}/${project}/`,
method: 'PUT',
data: {options: fieldData}, // 等效于getData
});
},
}}重要提示:为混合类型schema的mutation添加类型
当使用带有混合类型(如字符串和布尔值)schema的时,mutation函数必须使用schema推断的类型。使用等泛型类型会破坏TanStack Form根据字段名称缩小字段类型的能力。
AutoSaveFieldRecord<string, unknown>tsx
const preferencesSchema = z.object({
theme: z.string(),
language: z.string(),
notifications: z.boolean(),
});
type Preferences = z.infer<typeof preferencesSchema>;
// ❌ 不要使用泛型类型 - 会破坏字段类型缩小
mutationOptions={{
mutationFn: (data: Record<string, unknown>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}
// ✅ 使用schema推断的类型以实现正确的类型缩小
mutationOptions={{
mutationFn: (data: Partial<Preferences>) => {
return fetchMutation({url: '/user/', method: 'PUT', data: {options: data}});
},
}}mapFormErrors → setFieldErrors
setFieldErrorsmapFormErrors → setFieldErrors
setFieldErrorsThe function transformed API error responses into field-specific errors. In the new system, handle this in the catch block using .
mapFormErrorssetFieldErrorsOld:
tsx
// 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} {...}>New:
tsx
import {setFieldErrors} from '@sentry/scraps/form';
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {...},
validators: {onDynamic: schema},
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// Transform API errors and set on fields (equivalent to mapFormErrors)
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);
}
}
},
});Simpler pattern - For flat error responses:
tsx
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// API returns {email: ['Already taken'], username: ['Invalid']}
const errors = error.responseJSON;
if (errors) {
setFieldErrors(formApi, {
email: {message: errors.email?.[0]},
username: {message: errors.username?.[0]},
});
}
}
},Note:supports nested paths with dot notation:setFieldErrors'config.schedule': {message: 'Invalid schedule'}
mapFormErrorssetFieldErrors旧版写法:
tsx
// 表单级别的错误转换器
function mapMonitorFormErrors(responseJson?: any) {
if (responseJson.config === undefined) {
return responseJson;
}
// 将嵌套的config错误扁平化为点表示法
const {config, ...rest} = responseJson;
const configErrors = Object.fromEntries(
Object.entries(config).map(([key, value]) => [`config.${key}`, value])
);
return {...rest, ...configErrors};
}
<Form mapFormErrors={mapMonitorFormErrors} {...}>新版写法:
tsx
import {setFieldErrors} from '@sentry/scraps/form';
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {...},
validators: {onDynamic: schema},
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// 转换API错误并设置到字段上(等效于mapFormErrors)
const responseJson = error.responseJSON;
if (responseJson?.config) {
// 将嵌套错误扁平化为点表示法
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);
}
}
},
});更简洁的写法 - 针对扁平的错误响应:
tsx
onSubmit: async ({value, formApi}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// API返回 {email: ['Already taken'], username: ['Invalid']}
const errors = error.responseJSON;
if (errors) {
setFieldErrors(formApi, {
email: {message: errors.email?.[0]},
username: {message: errors.username?.[0]},
});
}
}
},注意:支持使用点表示法的嵌套路径:setFieldErrors'config.schedule': {message: 'Invalid schedule'}
saveMessage → onSuccess
onSuccesssaveMessage → onSuccess
onSuccessThe showed a custom toast/alert after successful save. In the new system, handle this in the mutation's callback.
saveMessageonSuccessOld:
tsx
{
name: 'fingerprintingRules',
saveOnBlur: false,
saveMessageAlertVariant: 'info',
saveMessage: t('Changing fingerprint rules will apply to future events only.'),
}New:
tsx
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
<AutoSaveField
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.'));
},
}}
>saveMessageonSuccess旧版写法:
tsx
{
name: 'fingerprintingRules',
saveOnBlur: false,
saveMessageAlertVariant: 'info',
saveMessage: t('Changing fingerprint rules will apply to future events only.'),
}新版写法:
tsx
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
<AutoSaveField
name="fingerprintingRules"
schema={schema}
initialValue={project.fingerprintingRules}
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onSuccess: () => {
// 自定义成功消息(等效于saveMessage)
addSuccessMessage(t('Changing fingerprint rules will apply to future events only.'));
},
}}
>formatMessageValue → onSuccess
onSuccessformatMessageValue → onSuccess
onSuccessThe controlled how the changed value appeared in success toasts. Setting it to disabled showing the value entirely (useful for large text fields). In the new system, you control this directly in .
formatMessageValuefalseonSuccessOld:
tsx
{
name: 'fingerprintingRules',
saveMessage: t('Rules updated'),
formatMessageValue: false, // Don't show the (potentially huge) value in toast
}New:
tsx
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));
},formatMessageValuefalseonSuccess旧版写法:
tsx
{
name: 'fingerprintingRules',
saveMessage: t('Rules updated'),
formatMessageValue: false, // 不要在提示框中显示(可能非常大的)值
}新版写法:
tsx
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onSuccess: () => {
// 只显示消息,不显示值(等效于formatMessageValue: false)
addSuccessMessage(t('Rules updated'));
},
}}
// 如果你想显示格式化后的值:
onSuccess: (data) => {
addSuccessMessage(t('Slug changed to %s', data.slug));
},resetOnError → onError
onErrorresetOnError → onError
onErrorThe option reverted fields to their previous value when a save failed. In the new system, call in the mutation's callback.
resetOnErrorform.reset()onErrorOld:
tsx
// Form-level reset on error
<Form resetOnError apiEndpoint="/auth/" {...}>
// Or field-level (BooleanField always resets on error)
<FormField resetOnError name="enabled" {...}>New (with useScrapsForm):
tsx
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
}
},
});New (with AutoSaveField):
tsx
<AutoSaveField
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: AutoSaveField with TanStack Query already handles error states gracefully - the mutation'sstate is reflected in the UI. Manual reset is typically only needed for specific UX requirements like password fields.isError
resetOnErroronErrorform.reset()旧版写法:
tsx
// 表单级别的错误时重置
<Form resetOnError apiEndpoint="/auth/" {...}>
// 或者字段级(BooleanField总是在错误时重置)
<FormField resetOnError name="enabled" {...}>新版写法(使用useScrapsForm):
tsx
const form = useScrapsForm({
...defaultFormOptions,
defaultValues: {password: ''},
validators: {onDynamic: schema},
onSubmit: async ({value}) => {
try {
await mutation.mutateAsync(value);
} catch (error) {
// 错误时将表单重置为之前的值(等效于resetOnError)
form.reset();
throw error; // 如果需要继续错误处理,重新抛出错误
}
},
});新版写法(使用AutoSaveField):
tsx
<AutoSaveField
name="enabled"
schema={schema}
initialValue={settings.enabled}
mutationOptions={{
mutationFn: data => fetchMutation({...}),
onError: () => {
// 字段会通过TanStack Query自动显示错误状态
// 如果需要重置值,可以传递重置回调
},
}}
>注意:结合TanStack Query的AutoSaveField已经可以优雅地处理错误状态 - mutation的状态会反映在UI中。手动重置通常仅适用于密码字段等特定的UX需求。isError
saveOnBlur: false → useScrapsForm
useScrapsFormsaveOnBlur: false → useScrapsForm
useScrapsFormFields with showed an inline alert with Save/Cancel buttons instead of auto-saving. This was used for dangerous operations (slug changes) or large text edits (fingerprint rules).
saveOnBlur: falseIn the new system, use a regular form with and an explicit Save button. This preserves the UX of showing warnings before committing.
useScrapsFormOld:
tsx
{
name: 'slug',
type: 'string',
saveOnBlur: false,
saveMessageAlertVariant: 'warning',
saveMessage: t("Changing a project's slug can break your build scripts!"),
}New:
tsx
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">
<Button onClick={() => form.reset()}>Cancel</Button>
<form.SubmitButton>Save</form.SubmitButton>
</Flex>
</form.AppForm>
);
}When to use this pattern:
- Dangerous operations where users should see a warning before committing (slug changes, security tokens)
- Large multiline text fields where you want to finish editing before saving (fingerprint rules, filters)
- Any field where auto-save doesn't make sense
设置的字段会显示带保存/取消按钮的内联通知,而不是自动保存。这适用于危险操作(如修改slug)或大型文本编辑(如指纹规则)。
saveOnBlur: false在新系统中,使用带的常规表单和显式保存按钮。这样可以保留提交前显示警告的UX。
useScrapsForm旧版写法:
tsx
{
name: 'slug',
type: 'string',
saveOnBlur: false,
saveMessageAlertVariant: 'warning',
saveMessage: t("Changing a project's slug can break your build scripts!"),
}新版写法:
tsx
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>
{/* 保存前显示警告(等效于saveMessage) */}
<Alert variant="warning">
{t("Changing a project's slug can break your build scripts!")}
</Alert>
<Flex gap="sm" justify="end">
<Button onClick={() => form.reset()}>Cancel</Button>
<form.SubmitButton>Save</form.SubmitButton>
</Flex>
</form.AppForm>
);
}何时使用此模式:
- 危险操作,用户应在提交前看到警告(如修改slug、安全令牌)
- 大型多行文本字段,需要完成编辑后再保存(如指纹规则、过滤器)
- 任何不适合自动保存的字段
Preserving Form Search Functionality
保留表单搜索功能
Sentry's SettingsSearch allows users to search for individual settings fields. When migrating forms, you must preserve this searchability by wrapping migrated forms with .
FormSearchSentry的SettingsSearch允许用户搜索单个设置字段。迁移表单时,必须通过用包裹迁移后的表单来保留此搜索功能。
FormSearchThe FormSearch
Component
FormSearchFormSearch
组件
FormSearchFormSearchroutetsx
import {FormSearch} from 'sentry/components/core/form';
<FormSearch route="/settings/account/details/">
<FieldGroup title={t('Account Details')}>
<AutoSaveField 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>
)}
</AutoSaveField>
</FieldGroup>
</FormSearch>Props:
| Prop | Type | Description |
|---|---|---|
| | The settings route for this form (e.g., |
| | The form content — rendered unchanged at runtime. |
Rules:
- The must match the settings page URL exactly (including trailing slash).
route - Wrap the entire form section with a single , not individual fields.
FormSearch - Every or
<AutoSaveField>inside a<form.AppField>will be indexed. Make sureFormSearchandlabelare plain string literals orhintTextcalls — computed/dynamic strings will be skipped by the extractor.t()
FormSearchroutetsx
import {FormSearch} from 'sentry/components/core/form';
<FormSearch route="/settings/account/details/">
<FieldGroup title={t('Account Details')}>
<AutoSaveField 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>
)}
</AutoSaveField>
</FieldGroup>
</FormSearch>属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| | 此表单的设置页面路由(例如: |
| | 表单内容 —— 运行时原样渲染。 |
规则:
- 必须与设置页面URL完全匹配(包括尾部斜杠)。
route - 用单个包裹整个表单区域,而不是单个字段。
FormSearch - 内的每个
FormSearch或<AutoSaveField>都会被索引。确保<form.AppField>和label是纯字符串字面量或hintText调用 —— 计算/动态字符串会被提取器跳过。t()
The Form Field Registry
表单字段注册表
After adding or updating wrappers, regenerate the field registry so that search results stay up to date:
FormSearchbash
pnpm run extract-form-fieldsThis script () scans all TSX files, finds components, extracts field metadata (, , , ), and writes the generated registry to . Commit this generated file alongside your migration PR — it is part of the source tree.
scripts/extractFormFields.ts<FormSearch>namelabelhintTextroutestatic/app/components/core/form/generatedFieldRegistry.tsRun the command after any change to forms inside awrapper (adds, removals, label changes). The generated file is checked in and should not be edited manually.FormSearch
添加或更新包裹后,重新生成字段注册表,使搜索结果保持最新:
FormSearchbash
pnpm run extract-form-fields此脚本()会扫描所有TSX文件,找到组件,提取字段元数据(、、、),并将生成的注册表写入。将此生成的文件与迁移PR一起提交 —— 它是源代码树的一部分。
scripts/extractFormFields.ts<FormSearch>namelabelhintTextroutestatic/app/components/core/form/generatedFieldRegistry.ts在包裹内的表单发生任何更改(添加、删除、标签更改)后运行此命令。生成的文件已纳入版本控制,不应手动编辑。FormSearch
Migration: Old Forms Already Searchable
迁移:已支持搜索的旧表单
If the legacy being migrated was already indexed by SettingsSearch (i.e., it had entries in ), you must add a wrapper to the new form so search functionality is preserved. The old and new sources coexist — new registry entries take precedence over old ones for the same route + field combination — but once you remove the legacy form the old entries will disappear.
JsonFormsentry/data/formsFormSearch如果正在迁移的旧版已被SettingsSearch索引(即它在中有条目),则必须为新表单添加包裹,以保留搜索功能。旧版和新版条目会共存 —— 对于相同的路由+字段组合,新版注册表条目优先,但一旦移除旧版表单,旧条目就会消失。
JsonFormsentry/data/formsFormSearchIntentionally Not Migrated
未迁移的功能
| Feature | Usage | Reason |
|---|---|---|
| 3 forms | Undo in toasts adds complexity with minimal benefit. Use simple error toasts instead. |
| 功能 | 使用次数 | 原因 |
|---|---|---|
| 3个表单 | 提示框中的撤销功能增加了复杂性,但收益极小。改用简单的错误提示框即可。 |
Migration Checklist
迁移检查清单
- Replace JsonForm/FormModel with useScrapsForm or AutoSaveField
- Convert field config objects to JSX AppField components
- Replace →
helpon layoutshintText - Replace →
showHelpInTooltipvariant="compact" - Replace →
disabledReasondisabled="reason string" - Replace → additional JSX in layout
extraHelp - Convert object to function:
confirm(value) => message | undefined - Handle in mutationFn
getData - Handle with setFieldErrors in catch
mapFormErrors - Handle in onSuccess callback
saveMessage - Convert fields to regular forms with Save button
saveOnBlur: false - Verify cache updates merge with existing data (use updater function) — some API endpoints may return partial objects
onSuccess - Wrap the migrated form with if the old form was searchable in SettingsSearch
<FormSearch route="..."> - Run and commit the updated
pnpm run extract-form-fieldsgeneratedFieldRegistry.ts
- 用useScrapsForm或AutoSaveField替换JsonForm/FormModel
- 将字段配置对象转换为JSX AppField组件
- 将布局上的替换为
helphintText - 将替换为
showHelpInTooltipvariant="compact" - 将替换为
disabledReasondisabled="原因字符串" - 将替换为布局中的额外JSX
extraHelp - 将对象转换为函数:
confirm(value) => 消息 | undefined - 在mutationFn中处理
getData - 在catch块中用setFieldErrors处理
mapFormErrors - 在onSuccess回调中处理
saveMessage - 将字段转换为带保存按钮的常规表单
saveOnBlur: false - 验证缓存更新是否与现有数据合并(使用更新器函数)—— 某些API端点可能返回部分对象
onSuccess - 如果旧表单在SettingsSearch中可搜索,用包裹迁移后的表单
<FormSearch route="..."> - 运行并提交更新后的
pnpm run extract-form-fieldsgeneratedFieldRegistry.ts