背景 / 目标
支持表单的级联录入(如国家 → 省 → 市)以及更一般的「选项按条件显隐」需求。
经过设计讨论,级联需求其实是两类 ,占比与正确解法完全不同,需要分开对待:
(a) 基于后端数据的级联 —— 已支持,无需开发
国/省/市为后端对象、组织(公司/部门/团队)、商品目录(类目/SKU)、account→contact 等。数据量大、会变、跨表单共享 —— 本就该是数据(lookup),不是静态枚举 。
这条路 objectui 已经完整实现 (#2215 ):lookup + depends_on,带门控 + 硬 $filter + explicit param 映射。
packages/fields/src/widgets/LookupField.tsx:265(depends_on 解析/门控/过滤)
packages/components/src/renderers/form/form.tsx:110,152,749(注入实时表单值 dependentValues)
⚠️ 结论:国/省/市这类标准场景的推荐答案就是已有的 lookup + depends_on 。本 issue 处理的是下面 (b) 这个更小、但真实的缺口。
(b) 基于静态枚举 / 上下文的选项显隐 —— 本 issue 要做的
小而稳定的语义化字典(工单类别→子类别、record type→可选状态),以及按用户角色/租户/时间显隐选项 。目前 SelectField(packages/fields/src/widgets/SelectField.tsx)是哑组件,只渲染静态 options,无法表达任何依赖。
设计决策(及理由)
采用「复用既有原语」而非新造 bespoke 机制:dependsOn + 选项级 visibleWhen。明确放弃 Salesforce 式 validFor / controllingField / 依赖矩阵。
理由:
统一 & 最小词汇 :dependsOn 已是 lookup 级联的声明方式,visibleWhen 已是字段级 CEL 谓词。picklist 级联 = dependsOn(声明依赖边)+ 选项 visibleWhen(条件),与 lookup 级联同构,不引入新范畴。
visibleWhen 是通用谓词,严格强于 cascade :它不止能判断兄弟字段,还能判断 current_user.roles、租户、now() 等 —— validFor 只能表达「随另一字段的值」,覆盖不了角色/上下文。而且 spec 已内建 这个能力:current_user(含 roles)暴露给每一个谓词面 (server formula、server RLS、client UI gates,形状一致 —— 见 framework packages/spec/src/identity/eval-user.zod.ts),已有先例 packages/spec/src/ui/app.zod.ts:49 的 P\'org_admin' in current_user.roles`。所以选项级 visibleWhen` 引用角色/上下文零新增绑定 。
面向 AI-authoring 优化 :未来 schema 主要由 AI 生成、人类阅读确认。visibleWhen: "country == 'cn'" 读起来是人话、且是模型见过无数次的原语 → 生成错误率低、人易确认。validFor 矩阵的主要价值(可视化编辑)在此 authoring 模型下不成立。
dependsOn 与 visibleWhen 正交 :dependsOn 只声明「依赖哪些同表单字段」(驱动级联门控 UX / 重算 / 依赖图);visibleWhen 是真正的谓词。一个选项可以只有 visibleWhen 没有 dependsOn (纯角色门)。不要把二者耦合。
⚠️ 安全线(必须明确)
在下拉里隐藏选项是 UX,不是安全边界 。若隐藏理由是「该用户无权选此值」(授权用途),则客户端隐藏之外必须服务端也拦 ,否则用户绕过 UI 直接提交即穿透。
平台架构本就是双端强制(client UI gate + server RLS/formula)。但当前 picklist 值是静态枚举、无 per-option 谓词,服务端目前没有任何东西会拦非法 option 值的写入 。因此授权用途需要新增服务端强制点 (rule-validator 写入时对被选值求值其 visibleWhen,false 则拒),对标已有的 stripReadonlyWhenFields。纯级联 / UX 用途不需要这条。
开发任务
Spec(objectstack-ai/framework,遵循其 worktree 规约)
objectui
表单渲染器把实时表单值(ruleRecord)转发给 select(以及 radio):stripRegisteredFieldProps 目前仅对 DATA_SOURCE_FIELD_TYPES 转发 dependentValues,需扩展(packages/components/src/renderers/form/form.tsx:110,152)。
SelectField 计算有效选项:对每个选项的 visibleWhen 用 @objectstack/formula 对「实时 record + current_user」求值过滤;dependsOn 父值为空时门控(复用现有 empty-state 分支,packages/fields/src/widgets/SelectField.tsx:39)。
镜像类型:SelectOptionMetadata 加 visibleWhen(packages/types/src/field-types.ts:261),并顺带收敛与 spec SelectOptionSchema 的漂移(objectui 多 icon/disabled,spec 多 default)。
级联清空:父值变化后,若子字段当前值已不在有效选项内,清空之(避免「中国-加州」脏数据)。建议在 widget 内做,多级链自动成立。
构建期 CEL 校验(护栏) :对选项 visibleWhen 引用的字段名/字面量,对照被引用字段的值域做静态检查,在构建期抓出 AI 写错的字面量(如 country=='chna')。
服务端 option 值强制(仅授权用途) :rule-validator 拒绝写入 visibleWhen=false 的选项值,对标 stripReadonlyWhenFields。
文档
例子 / Demo
Skills(面向 AI 生成)
备注
背景 / 目标
支持表单的级联录入(如国家 → 省 → 市)以及更一般的「选项按条件显隐」需求。
经过设计讨论,级联需求其实是两类,占比与正确解法完全不同,需要分开对待:
(a) 基于后端数据的级联 —— 已支持,无需开发
国/省/市为后端对象、组织(公司/部门/团队)、商品目录(类目/SKU)、account→contact 等。数据量大、会变、跨表单共享 —— 本就该是数据(lookup),不是静态枚举。
这条路 objectui 已经完整实现(#2215):
lookup+depends_on,带门控 + 硬$filter+ explicit param 映射。packages/fields/src/widgets/LookupField.tsx:265(depends_on 解析/门控/过滤)packages/components/src/renderers/form/form.tsx:110,152,749(注入实时表单值dependentValues)(b) 基于静态枚举 / 上下文的选项显隐 —— 本 issue 要做的
小而稳定的语义化字典(工单类别→子类别、record type→可选状态),以及按用户角色/租户/时间显隐选项。目前
SelectField(packages/fields/src/widgets/SelectField.tsx)是哑组件,只渲染静态options,无法表达任何依赖。设计决策(及理由)
采用「复用既有原语」而非新造 bespoke 机制:
dependsOn+ 选项级visibleWhen。明确放弃 Salesforce 式validFor/controllingField/ 依赖矩阵。理由:
统一 & 最小词汇:
dependsOn已是 lookup 级联的声明方式,visibleWhen已是字段级 CEL 谓词。picklist 级联 =dependsOn(声明依赖边)+ 选项visibleWhen(条件),与 lookup 级联同构,不引入新范畴。visibleWhen是通用谓词,严格强于 cascade:它不止能判断兄弟字段,还能判断current_user.roles、租户、now()等 ——validFor只能表达「随另一字段的值」,覆盖不了角色/上下文。而且 spec 已内建这个能力:current_user(含roles)暴露给每一个谓词面(server formula、server RLS、client UI gates,形状一致 —— 见 frameworkpackages/spec/src/identity/eval-user.zod.ts),已有先例packages/spec/src/ui/app.zod.ts:49的P\'org_admin' in current_user.roles`。所以选项级visibleWhen` 引用角色/上下文零新增绑定。面向 AI-authoring 优化:未来 schema 主要由 AI 生成、人类阅读确认。
visibleWhen: "country == 'cn'"读起来是人话、且是模型见过无数次的原语 → 生成错误率低、人易确认。validFor矩阵的主要价值(可视化编辑)在此 authoring 模型下不成立。dependsOn与visibleWhen正交:dependsOn只声明「依赖哪些同表单字段」(驱动级联门控 UX / 重算 / 依赖图);visibleWhen是真正的谓词。一个选项可以只有visibleWhen没有dependsOn(纯角色门)。不要把二者耦合。在下拉里隐藏选项是 UX,不是安全边界。若隐藏理由是「该用户无权选此值」(授权用途),则客户端隐藏之外必须服务端也拦,否则用户绕过 UI 直接提交即穿透。
平台架构本就是双端强制(client UI gate + server RLS/formula)。但当前 picklist 值是静态枚举、无 per-option 谓词,服务端目前没有任何东西会拦非法 option 值的写入。因此授权用途需要新增服务端强制点(rule-validator 写入时对被选值求值其
visibleWhen,false 则拒),对标已有的stripReadonlyWhenFields。纯级联 / UX 用途不需要这条。开发任务
Spec(objectstack-ai/framework,遵循其 worktree 规约)
SelectOptionSchema新增visibleWhen: ExpressionInputSchema.optional()(packages/spec/src/data/field.zod.ts第 88 行附近)。因 dataField.options与 viewFormField.options共用此 schema,一处生效两层。Field.dependsOn的描述改为机制中立(当前为 "Dependent lookup…" 措辞,packages/spec/src/data/field.zod.ts:495),{field,param}注明为 lookup-only 细化。类型不变。objectui
ruleRecord)转发给select(以及radio):stripRegisteredFieldProps目前仅对DATA_SOURCE_FIELD_TYPES转发dependentValues,需扩展(packages/components/src/renderers/form/form.tsx:110,152)。SelectField计算有效选项:对每个选项的visibleWhen用@objectstack/formula对「实时 record + current_user」求值过滤;dependsOn父值为空时门控(复用现有 empty-state 分支,packages/fields/src/widgets/SelectField.tsx:39)。SelectOptionMetadata加visibleWhen(packages/types/src/field-types.ts:261),并顺带收敛与 specSelectOptionSchema的漂移(objectui 多icon/disabled,spec 多default)。visibleWhen引用的字段名/字面量,对照被引用字段的值域做静态检查,在构建期抓出 AI 写错的字面量(如country=='chna')。visibleWhen=false的选项值,对标stripReadonlyWhenFields。文档
lookup + depends_on;静态/角色显隐走option.visibleWhen)、何时用哪条、以及安全线(授权用途必须服务端强制)。SelectOption.visibleWhen及可用变量(record/current_user/now()…)。例子 / Demo
lookup + depends_on数据版(推荐路径),(2)option.visibleWhen静态枚举版。visibleWhen: "'admin' in current_user.roles",并演示对应的服务端强制。Skills(面向 AI 生成)
dependsOn+visibleWhen,不生造validFor。备注
objectui与framework(spec)两仓;spec 改动应先落、objectui 再镜像(与dependsOn/visibleWhen的既有对齐方式一致)。