Odoo 里很多“看起来是图形化配置”的界面,底层其实都要落成 domain:
- 高级筛选;
- 自动化规则;
- 智能按钮条件;
- 某些视图里的动态过滤器。
问题在于,domain 本身是程序员友好的表达,不是业务用户友好的表达。
所以前端必须解决一个很棘手的任务:
把一段 domain 字符串重新还原成可以点击、改字段、换操作符、改布尔连接词的可视树。
这就是 DomainSelector 的职责。
一、DomainSelector 的第一步不是渲染 UI,而是先确认“这段 domain 支不支持被还原”
onPropsUpdated() 一开头就会尝试:
domain = new Domain(p.domain)
如果这里直接失败,组件不会硬着头皮渲染一个半残 UI,而是:
tree = nullshowArchivedCheckbox = falseincludeArchived = false
然后模板层显示“This domain is not supported.”。
这一步看似保守,实际上很重要。
因为 domain 不是任何时候都能无损映射回树编辑器。某些复杂写法、调试态表达或暂时不支持的语法,如果前端强行解析,用户只会在可视界面里得到一个被静默改写的结果。
官方宁愿直说“我不支持”,也不偷偷给你改语义,这是对的。
二、真正的还原工作不在组件本身,而是交给 tree_processor service
当 domain 可解析后,DomainSelector 会并行做两件事:
this.treeProcessor.treeFromDomain(...)this.fieldService.loadFieldInfo(p.resModel, "active")
前者把 domain 还原成树,后者则判断模型上有没有 active 字段。
这两步组合起来,就形成了 DomainSelector 的真实输入:
- 一棵可编辑条件树;
- 一份模型字段信息;
- 一条额外的业务边界:是否支持 archived 切换。
因此 DomainSelector 并不是“自己懂 domain 的全部细节”,它更像一个装配层:
- 解析交给 domain / tree processor;
- 字段元数据交给 field service;
- 最终 UI 组合和交互边界由自己控制。
三、getDomainDisplayedOperators() 解释了为什么不同字段类型看到的操作符不一样
这是这组源码里最有价值的部分之一。
getDomainDisplayedOperators(fieldDef) 没有把所有操作符都暴露给用户,而是按字段类型收缩:
boolean只有set / not setdate/datetime有in range, =, <, >, set, not setchar/text偏向ilike,starts withmany2one/many2many会给in / not in / ilike
这背后是很明确的产品判断:
Domain 的语法能力很强,但图形化编辑器不该把所有可能性都原样暴露。
为什么?因为“语法允许”不等于“交互上合适”。
如果一个布尔字段也给出 like、>、in range 之类操作符,用户虽然 technically 能看到更多选项,但多数只会带来误解。
所以 DomainSelector 真正做的是操作符归一化:
- 对前端展示层,缩成合理集合;
- 对不同字段类型,给出最能解释业务意义的候选。
四、默认条件不是拍脑袋生成的,而是沿着“默认字段 -> 默认操作符 -> 默认值”三段走完
utils.js 里的 getDefaultCondition(fieldDefs) 很短,但很有代表性。
它会依次调用:
getDefaultPath(fieldDefs)getDomainDisplayedOperators(fieldDef)[0]getDefaultValue(fieldDef, operator)
最终拼成 condition(fieldDef.name, operator, value)。
也就是说,新增一条叶子条件时,前端并不是随便塞个空壳,而是明确遵守下面这条链路:
- 先找到一个最适合起步的字段;
- 再为这个字段挑最默认、最安全的操作符;
- 最后给出与字段类型和操作符匹配的默认值。
这就是为什么 DomainSelector 的“新增条件”体验通常不会太突兀——它给你的不是裸空白,而是一个能继续编辑的最小合法条件。
五、archived 不是普通条件,而是被折叠成一个专用业务开关
源码里对 archived 的处理非常值得学习。
它预定义了:
ARCHIVED_CONDITION = condition("active", "in", [true, false])ARCHIVED_DOMAIN = '[("active", "in", [True, False])]'
然后在 onPropsUpdated() 中主动把这条条件从 & 连接树里剥离出来,映射为 includeArchived 布尔开关。
这相当于做了一次“语义提升”:
- 底层仍然是 domain 条件;
- 但 UI 层不让用户每次都手写
active in [true, false]; - 而是给一个更符合认知的复选框:
Include archived。
这件事说明一个很重要的前端原则:
不是所有 domain 叶子都应该原样暴露,有些通用业务语义值得被提炼成独立控件。
六、update(tree) 把树重新压回 domain 字符串时,顺手补回 archived 边界
update(tree) 的逻辑也很干净:
- 若
includeArchived为真,先构造archiveDomain; - 若有树,就
Domain.and([domainFromTree(tree), archiveDomain]).toString(); - 若没有树,就直接退回 archived 边界本身。
这说明 archived 开关不是 UI 装饰,而是序列化阶段的一等公民。
也就是说,DomainSelector 并不是“编辑完树再额外拼个 checkbox 状态”,而是从一开始就把它当成完整 domain 的组成部分。
七、模板层暴露了双轨编辑模式:可视树 + 调试代码框
domain_selector.xml 很值得一看,因为它把两条编辑路径同时留着:
- 普通模式看
TreeEditor - debug 模式下还能直接看到 code editor textarea
这种双轨设计特别符合 Odoo 的受众结构:
- 普通实施或业务用户可以用图形化树;
- 开发者 / 顾问可以直接看 domain 文本;
- 只读和可编辑模式都能共存。
这不是重复造轮子,而是在承认:domain 这种对象天然就有“业务可视化”和“技术原文”两种查看需求。
八、二开最容易踩的坑
误区 1:把所有操作符都一股脑扔给用户
这通常不是更强,而是更乱。
误区 2:只会从 tree 生成 domain,不会从 domain 还原 tree
这样用户一旦回到编辑界面,就只能看到原始字符串,体验会断层。
误区 3:把 archived 当普通条件叶子处理
用户真正想表达的是“包含归档记录”,不是“我会写 active in [true, false]”。
误区 4:不区分 debug 与普通交互模式
开发者需要原文,业务用户不一定需要。把两者混成一个界面,往往两边都不满意。
九、结论
DomainSelector 的价值,不在于它把 domain “显示出来”,而在于它做了三层关键翻译:
- 把字符串 domain 还原成可编辑树;
- 把过宽的语法能力收缩成类型友好的操作符集合;
- 把通用业务语义(如 archived)提升成独立控件。
所以更准确地说,Odoo 的 DomainSelector 是一个domain 语义适配器:
它连接的是底层表达能力与上层可视交互,而不只是一个筛选条件编辑框。
DISCUSSION
评论区