Skip to content

fix(): 优化动态表单项导出#547

Closed
zouli520 wants to merge 1 commit into
easyops-cn:masterfrom
zouli520:vozouli/dev
Closed

fix(): 优化动态表单项导出#547
zouli520 wants to merge 1 commit into
easyops-cn:masterfrom
zouli520:vozouli/dev

Conversation

@zouli520

@zouli520 zouli520 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

依赖检查

组件之间的依赖声明,是微服务组件架构下的重要信息,请确保其正确性。

请勾选以下两组选项其中之一:

  • 本次 MR 没有使用上游组件(例如框架、后台组件等)的较新版本提供的特性。

或者:

  • 本次 MR 使用了上游组件(例如框架、后台组件等)的较新版本提供的特性。
  • 在对应的文件中更新了该上游组件的依赖版本(或确认了当前声明的依赖版本已包含本次 MR 使用的新特性)。

提交信息检查

Git 提交信息将决定包的版本发布及自动生成的 CHANGELOG,请检查工作内容与提交信息是否相符,并在以下每组选项中都依次确认。

破坏性变更是针对于下游使用者而言,可以通过本次改动对下游使用者的影响来识别变更类型:

  • 下游使用者不做任何改动,仍可以正常工作时,那么它属于普通变更。
  • 反之,下游使用者不做改动就无法正常工作时,那么它属于破坏性变更。

例如,构件修改了一个属性名,小产品 Storyboard 中需要使用新属性名才能工作,那么它就是破坏性变更。
又例如,构件还没有任何下游使用者,那么它的任何变更都是普通变更。

破坏性变更:

  • ⚠️ 本次 MR 包含破坏性变更的提交,请继续确认以下所有选项:
  • 没有更好的兼容方案,必须做破坏性变更。
  • 使用了 feat 作为提交类型。
  • 标注了 BREAKING CHANGE: 你的变更说明
  • 同时更新了本仓库中所有下游使用者的调用。
  • 同时更新了本仓库中所有下游使用者对该子包的依赖为即将发布的 major 版本。
  • 同时为其它仓库的 Migrating 做好了准备,例如文档或批量改动的方法。
  • 手动验证过破坏性变更在 Migrate 后可以正常工作。
  • 破坏性变更所在的提交没有意外携带其它子包的改动。

新特性:

  • 本次 MR 包含新特性的提交,且该提交不带有破坏性变更,并使用了 feat 作为提交类型。
  • 给新特性添加了单元测试。
  • 手动验证过新特性可以正常工作。

问题修复:

  • 本次 MR 包含问题修复的提交,且该提交不带有新特性或破坏性变更,并使用了 fix 作为提交类型。
  • 给问题修复添加了单元测试。
  • 手动验证过问题修复得到解决。

杂项工作:

即所有对下游使用者无任何影响、且没有必要显示在 CHANGELOG 中的改动,例如修改注释、测试用例、开发文档等:

  • 本次 MR 包含杂项工作的提交,且该提交不带有问题修复、新特性或破坏性变更,并使用了 chore, docs, test 等作为提交类型。

Summary by CodeRabbit

Release Notes

  • 功能优化
    • 改进表单数据导出功能,对多选、级联等复杂字段进行智能格式化处理,导出更清晰。
    • 增强表单验证功能,验证器现可访问当前列配置信息,提升验证的上下文识别能力。

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

总览

本 PR 通过三个相关联的变更增强动态表单的校验和导出功能。首先在类型层定义校验器回调参数新增 column 字段;其次在 ColumnComponent 中实现该参数的注入并更新依赖追踪;最后在导出模块中实现根据列类型对导出数据的类型感知格式化。

变更说明

表单校验和导出功能增强

层级 / 文件 说明
校验器参数类型定义
bricks/forms/src/interfaces/dynamic-form-item-v2.ts
BasicColumnrules[].validator(...) 回调参数 fullValue 中新增可选属性 column?: Column,使校验器可访问当前列的配置信息。
列组件中的校验器参数传递
bricks/forms/src/dynamic-form-item-v2/ColumnComponent.tsx
自定义 rule.validator 被调用时额外注入当前 column 对象到上下文参数;同时在负责计算 rulesuseMemo 依赖数组中增加 column,确保列配置变化时校验规则重新计算。
导出功能的数据格式化实现
bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx
exportFormData 在列头构建时记录每列的 typeprops.mode,导出行数据时根据列类型对数组值进行类型分支处理:select 多选/标签与其它通用数组转为逗号分隔字符串,cascader 先通过 flattenArray 递归展平再转为逗号分隔字符串,非数组值直接写入导出行。

审查工作量评估

🎯 2 (Simple) | ⏱️ ~12 分钟

相关 PR

  • easyops-cn/next-basics#541:在同一文件 excelUtils.tsxexportFormData 中基于列的 typeprops.mode 实现导出格式化,并使用类似的数组展平和字符串化逻辑。

  • easyops-cn/next-basics#493:本 PR 中对 exportFormData 的增强直接构建在该 PR 引入的导出功能基础之上。

建议标签

eve: release after merged

建议审查者

  • gdutzhendong
  • tcsuhaibo
  • lijmyeah
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR描述包含完整的模板结构,已正确勾选依赖检查和问题修复部分,但缺少具体的变更说明和测试验证记录。 建议补充简要说明本次优化的具体内容(如导出字段类型处理、校验器上下文改进等)和手动验证结果。
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 标题明确指出本次变更是针对动态表单项导出功能的优化修复,与代码变更内容(导出逻辑改进、校验器上下文增强)相符。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bricks/forms/src/dynamic-form-item-v2/ColumnComponent.tsx (1)

117-151: ⚠️ Potential issue | 🟡 Minor

实现正确;但测试仍未覆盖 column 传递
代码在自定义校验器回调上下文中传入了 { formValue, rowValue, rowIndex, column },并将 column 加入 useMemo 依赖;但 ColumnComponent.spec.tsxit("validator should work") 目前只断言了 { formValue, rowIndex, rowValue },未断言 fullValue.column,无法保证新增的 column 参数传递正确。

建议在该用例中补充对 column 的断言(例如将期望对象扩展为包含 column)。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bricks/forms/src/dynamic-form-item-v2/ColumnComponent.tsx` around lines 117 -
151, The test it("validator should work") in ColumnComponent.spec.tsx currently
asserts the validator callback receives { formValue, rowIndex, rowValue } but
doesn't check the injected column; update the spec to expect the full context
passed by ColumnComponent's useMemo validator wrapper (the fourth argument to
rule.validator) to include column as well — i.e., call the validator in the test
and assert the received context object contains formValue, rowValue, rowIndex
and the same column object (from the test fixture) so the column parameter
delivered by ColumnComponent.rule.validator is covered.
🧹 Nitpick comments (1)
bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx (1)

44-45: ⚡ Quick win

建议使用类型安全的方式访问列属性。

使用 as any 绕过了 TypeScript 的类型检查。由于 Column 是一个带有类型判别器的联合类型,可以通过类型守卫或更精确的类型断言来安全访问这些属性。

♻️ 建议的类型安全改进
  const headers = columns.map((col) => ({
    key: col.name,
    header: col.label || col.name,
-   type: (col as any).type,
-   mode: (col as any).props?.mode,
+   type: col.type,
+   mode: col.type === 'select' ? col.props?.mode : undefined,
  }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx` around lines 44 - 45,
The code currently uses (col as any).type and (col as any).props?.mode which
bypasses TypeScript checks; replace these casts with a proper type guard to
narrow the Column union before accessing properties: implement a predicate
(e.g., isColumnWithProps or similar) that checks the Column discriminant and/or
presence of props, then access col.type and col.props.mode only after narrowing;
update the usage sites in excelUtils.tsx (the variables col, type, mode) to rely
on that guard or a precise type assertion (e.g., ColumnWithProps) instead of as
any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx`:
- Around line 52-68: The export logic in excelUtils.tsx is not symmetric with
validateAndTransformValue: update the array handling in the export loop (where
exportRow[header.header] is assigned) so select multiple values are joined using
the same separator expected by validateAndTransformValue (use ',' with no space
or ensure import trims surrounding spaces) and make cascader exports consistent
with import (either export nested cascader values as JSON.stringify(...) or join
with '/' to match the import parsing); reference the header.type/header.mode
checks, flattenArray, and validateAndTransformValue to ensure round-trip
consistency.
- Around line 52-68: Array elements are being joined directly which yields
"[object Object]" for objects and "null"/"undefined" strings for missing values;
update the Array.isArray(value) branches in excelUtils.tsx (the block handling
header.type === 'select' / 'cascader' / other arrays where value = value.join(',
')) to first map/format each element: for cascader use flattenArray(...) then
map each element to a safe string (convert null/undefined to empty string,
primitives via String(), and objects via JSON.stringify or a custom display
field) and then join with ', '; ensure the same formatting logic is applied for
select (multiple/tags) and the generic array branch before assigning
exportRow[header.header] = value.

---

Outside diff comments:
In `@bricks/forms/src/dynamic-form-item-v2/ColumnComponent.tsx`:
- Around line 117-151: The test it("validator should work") in
ColumnComponent.spec.tsx currently asserts the validator callback receives {
formValue, rowIndex, rowValue } but doesn't check the injected column; update
the spec to expect the full context passed by ColumnComponent's useMemo
validator wrapper (the fourth argument to rule.validator) to include column as
well — i.e., call the validator in the test and assert the received context
object contains formValue, rowValue, rowIndex and the same column object (from
the test fixture) so the column parameter delivered by
ColumnComponent.rule.validator is covered.

---

Nitpick comments:
In `@bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx`:
- Around line 44-45: The code currently uses (col as any).type and (col as
any).props?.mode which bypasses TypeScript checks; replace these casts with a
proper type guard to narrow the Column union before accessing properties:
implement a predicate (e.g., isColumnWithProps or similar) that checks the
Column discriminant and/or presence of props, then access col.type and
col.props.mode only after narrowing; update the usage sites in excelUtils.tsx
(the variables col, type, mode) to rely on that guard or a precise type
assertion (e.g., ColumnWithProps) instead of as any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf97c72c-9bc3-4b54-a35f-d89a5d9f2764

📥 Commits

Reviewing files that changed from the base of the PR and between 0185086 and 94d4260.

📒 Files selected for processing (3)
  • bricks/forms/src/dynamic-form-item-v2/ColumnComponent.tsx
  • bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx
  • bricks/forms/src/interfaces/dynamic-form-item-v2.ts

Comment on lines +52 to +68
let value = row[header.key];

// 处理数组类型的值(如 select 多选、cascader 多选等)
if (Array.isArray(value)) {
// 对于多选模式,将数组转换为逗号分隔的字符串
if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
value = value.join(', ');
} else if (header.type === 'cascader') {
// Cascader 可能是多维数组,递归处理
value = flattenArray(value).join(', ');
} else {
// 其他数组类型也转换为字符串
value = value.join(', ');
}
}

exportRow[header.header] = value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

导出和导入逻辑不一致,可能导致数据往返丢失。

当前导出逻辑与 validateAndTransformValue 中的导入逻辑存在不对称:

  1. Select 多选模式:导出使用 ", " 连接,但导入时会按逗号或换行符分割(第 282 行)。导出的 ", " 中包含空格,导入时可能产生额外的空白字符。
  2. Cascader:导出时将嵌套数组展平后用 ", " 连接,但导入时会按 "/" 分割(第 263 行)或解析 JSON(第 255 行)。这会导致导出后无法正确还原原始的嵌套结构。

建议统一导出和导入的分隔符及数据格式,确保数据能够正确往返。

💡 建议的一致性改进方案

对于 cascader,建议导出为 JSON 字符串或使用与导入一致的 "/" 分隔符:

  } else if (header.type === 'cascader') {
-   // Cascader 可能是多维数组,递归处理
-   value = flattenArray(value).join(', ');
+   // Cascader 导出为 JSON 格式,与导入逻辑保持一致
+   try {
+     value = JSON.stringify(value);
+   } catch {
+     value = '';
+   }
  } else {

对于 select 多选,确保导入时正确处理空格:

  if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
-   value = value.join(', ');
+   value = value.join(','); // 移除空格,与导入逻辑一致
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let value = row[header.key];
// 处理数组类型的值(如 select 多选、cascader 多选等)
if (Array.isArray(value)) {
// 对于多选模式,将数组转换为逗号分隔的字符串
if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
value = value.join(', ');
} else if (header.type === 'cascader') {
// Cascader 可能是多维数组,递归处理
value = flattenArray(value).join(', ');
} else {
// 其他数组类型也转换为字符串
value = value.join(', ');
}
}
exportRow[header.header] = value;
let value = row[header.key];
// 处理数组类型的值(如 select 多选、cascader 多选等)
if (Array.isArray(value)) {
// 对于多选模式,将数组转换为逗号分隔的字符串
if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
value = value.join(','); // 移除空格,与导入逻辑一致
} else if (header.type === 'cascader') {
// Cascader 导出为 JSON 格式,与导入逻辑保持一致
try {
value = JSON.stringify(value);
} catch {
value = '';
}
} else {
// 其他数组类型也转换为字符串
value = value.join(', ');
}
}
exportRow[header.header] = value;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx` around lines 52 - 68,
The export logic in excelUtils.tsx is not symmetric with
validateAndTransformValue: update the array handling in the export loop (where
exportRow[header.header] is assigned) so select multiple values are joined using
the same separator expected by validateAndTransformValue (use ',' with no space
or ensure import trims surrounding spaces) and make cascader exports consistent
with import (either export nested cascader values as JSON.stringify(...) or join
with '/' to match the import parsing); reference the header.type/header.mode
checks, flattenArray, and validateAndTransformValue to ensure round-trip
consistency.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

数组值可能包含非原始类型,导致导出结果不正确。

当数组中包含对象时,.join(', ') 会产生 "[object Object]" 这样无意义的字符串。另外,数组中的 nullundefined 会被转换为字符串 "null""undefined"

建议在执行 join 操作前,先对数组元素进行格式化处理,确保转换为有意义的字符串表示。

🛡️ 建议的修复方案
  if (Array.isArray(value)) {
+   // 将数组元素转换为字符串,处理 null/undefined/对象
+   const formatArrayItem = (item: any): string => {
+     if (item === null || item === undefined) return '';
+     if (typeof item === 'object') return JSON.stringify(item);
+     return String(item);
+   };
+   
    // 对于多选模式,将数组转换为逗号分隔的字符串
    if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
-     value = value.join(', ');
+     value = value.map(formatArrayItem).filter(Boolean).join(', ');
    } else if (header.type === 'cascader') {
      // Cascader 可能是多维数组,递归处理
-     value = flattenArray(value).join(', ');
+     value = flattenArray(value).map(formatArrayItem).filter(Boolean).join(', ');
    } else {
      // 其他数组类型也转换为字符串
-     value = value.join(', ');
+     value = value.map(formatArrayItem).filter(Boolean).join(', ');
    }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let value = row[header.key];
// 处理数组类型的值(如 select 多选、cascader 多选等)
if (Array.isArray(value)) {
// 对于多选模式,将数组转换为逗号分隔的字符串
if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
value = value.join(', ');
} else if (header.type === 'cascader') {
// Cascader 可能是多维数组,递归处理
value = flattenArray(value).join(', ');
} else {
// 其他数组类型也转换为字符串
value = value.join(', ');
}
}
exportRow[header.header] = value;
let value = row[header.key];
// 处理数组类型的值(如 select 多选、cascader 多选等)
if (Array.isArray(value)) {
// 将数组元素转换为字符串,处理 null/undefined/对象
const formatArrayItem = (item: any): string => {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return JSON.stringify(item);
return String(item);
};
// 对于多选模式,将数组转换为逗号分隔的字符串
if (header.type === 'select' && (header.mode === 'multiple' || header.mode === 'tags')) {
value = value.map(formatArrayItem).filter(Boolean).join(', ');
} else if (header.type === 'cascader') {
// Cascader 可能是多维数组,递归处理
value = flattenArray(value).map(formatArrayItem).filter(Boolean).join(', ');
} else {
// 其他数组类型也转换为字符串
value = value.map(formatArrayItem).filter(Boolean).join(', ');
}
}
exportRow[header.header] = value;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bricks/forms/src/dynamic-form-item-v2/excelUtils.tsx` around lines 52 - 68,
Array elements are being joined directly which yields "[object Object]" for
objects and "null"/"undefined" strings for missing values; update the
Array.isArray(value) branches in excelUtils.tsx (the block handling header.type
=== 'select' / 'cascader' / other arrays where value = value.join(', ')) to
first map/format each element: for cascader use flattenArray(...) then map each
element to a safe string (convert null/undefined to empty string, primitives via
String(), and objects via JSON.stringify or a custom display field) and then
join with ', '; ensure the same formatting logic is applied for select
(multiple/tags) and the generic array branch before assigning
exportRow[header.header] = value.

@zouli520 zouli520 closed this Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant