Skip to content

Commit ede5a8e

Browse files
baozhoutaoclaude
andauthored
feat(showcase): 动作显隐矩阵——四个动作面 × 三种 visible 写法 × requiredPermissions × 每种字段类型的谓词 (#6157)
* feat(showcase): 动作显隐矩阵——四个动作面 × 三种 visible 写法 × requiredPermissions × 每种字段类型的谓词 给 `showcase_field_zoo` 加一整套可证伪的动作门控夹具,覆盖决定「按钮出不出现」的两件事: `visible`(记录上的 CEL 谓词)与 `requiredPermissions`(ADR-0066 D4 能力门)。 两条种子记录是可证伪性的来源:Specimen — Full(字段全填)必须出现, Specimen — Minimal(多数为 null)必须消失——写错的表现是按钮出现在错误的地方, 而不是一个悄悄变绿的测试。 - **四面一致性**:同一个动作声明在行内 kebab / 记录页头 / ⋯ 菜单,并被视图的 `bulkActions` 提升进批量栏。关系比关系(`f_lookup == f_lookups[0]`)是最利的 一条:两个操作数都是 lookup,谁被展开谁就变成对象,比较就静默为假。 `gated_columns` 视图把 `f_lookup` 设为列(于是被 `populate=`),默认列表两个都不展开 —— 同样的记录、同样的谓词,两个视图必须给出同一结论(objectui#3501)。 - **三种 visible 写法**:裸 CEL 字符串 / `P` 标签模板 / `{ dialect, source }` 信封, 同一条谓词并排钉住;三者若在同一条记录上分歧,就是某个渲染器把某一种当特例了。 布尔 visible 不在此列——`ExpressionInput` 不接受它,`objectstack build` 发不出这种 形状,由 objectui 自己的单测钉(objectui#3492)。 - **requiredPermissions**:持有 / 不持有 / 两个取 AND / 空声明四种,四个面同一结论。 新增 `showcase.restricted_ops` 能力:**定义了但不授予任何权限集**——门的可证伪一半, 正是批量栏当初整个忽略 `requiredPermissions` 时无人察觉的原因。 - **字段类型谓词园**:每种字段类型一条 `visible`,含 lookup(单/多/空)、master_detail、 tree、user、文本族、数值族、时间族、布尔/选项族、json/location/address/composite/ repeater/vector,以及 AND/OR/NOT/三元。全部对着真实 runtime 的 CEL 引擎实测过, 不是照着规范猜的。 顺带钉住三条实测得到的作者规则:必须 `record.` 前缀;遍历/调方法前必须判空 (`null` 没有成员也没有方法,不判空在 Minimal 上直接 fault);用 `contains()` / `matches()` 而不是 `startsWith()` / `endsWith()`(后者不是 CEL,objectui 会降级到遗留 JS 引擎并告警)。 `showcase_zoo_dialect_split` 是故意留下的**红色夹具**:同一个 `.contains()` 谓词在行内 kebab 里正常、在记录页头被判抛错并隐藏——页头不走 CEL 引擎(objectui#3521)。 按仓库规矩不粉饰,改成一屏可对比的现场。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(showcase): 新声明的动作/视图标签当场补 zh-CN 翻译,不抬 i18n 棘轮 `check-i18n-coverage` 把本示例的未翻译计数冻结在 451,新声明的标签一旦跳过 zh-CN 就把计数顶上去、门变红 —— 我加了 ~56 个动作标签和 3 个视图标签却一条没译, CI 报 451 → 572。 翻译而不是抬基线:那个脚本的抬头写明「debt is FROZEN, and the build fails the moment it grows」,本文件里既有的两条注释也各自写着「Translated at birth ... this example is ratcheted at its current untranslated count」。抬基线正是它防的 那件事。 补齐 `zh-CN.objects.showcase_field_zoo` 下的 `_actions`(12 条门控动作 + 44 条 字段类型谓词,含 3 条 successMessage)与 `_views`(3 个视图标签)。标签本身是覆盖 夹具的说明文字,中文照写同一句技术陈述,不做意译。 顺带修 `field-zoo.view.ts` 里一处缩进(上一笔批量替换留下的)。 现在 `check-i18n-coverage: OK (none new)`,基线仍是 451。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(showcase): `_views` 默认列表按运行时键 `default` 翻译,不是 `list` 合入 main 后 CI 仍红 451 → 452,本地却绿 —— 差别在于 CI 跑的是**分支与 main 的 合并结果**,而分支落后 main 136 个提交,其中 b3c1f3c#5164)改了 `_views` 的 键法:不带 `name` 声明的容器默认列表被 composer 注册为 `<object>.default`, bundle 的键就是这个裸运行时键。我写的 `_views.list` 是没有任何查找能到达的拼法, 正是 #5164 修掉的那个。 `ContactViews` 的注释里已经把这条定案写清楚了,我照它对齐。 顺带记下这次的教训并写进注释:**落后 main 的分支上「本地绿」不作数** —— 这条 只有在合并结果上才暴露。 合并后复跑:`check-i18n-coverage: OK (none new)`、`pnpm validate` exit 0、 `typecheck` 通过、146 tests passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(showcase): 加回 `record.owner_id == os.user.id` 归属门,并纠正一段已经过期的注释 合 main 之后重新实测:这条谓词现在**校验通过**。#5378 已经把答案放进 `packages/spec/src/data/injected-system-columns.ts`,让作者时校验能推导出平台注入 列 —— 我原先注释里写的「校验器拒绝它,已上报」在合并后的树上是**假话**,不能就 这么随 PR 发出去。 于是不只是改注释,而是把这条谓词作为正式标本加回来(`showcase_zoo_owner_gate` + 类型园里的 `showcase_zoo_t_owner`,三个记录面 + 两个视图的 `bulkActions`)。 它值得独立一条,因为 `owner_id` 是注入列而非声明字段,同时压两个面: - 作者时:它就是 #5378 落地的活证据; - 运行时:列表 `$select` 按对象声明字段构造,消费方若不认识平台列就会把它当拼错 丢掉、让谓词在缺席键上 fault —— 即 objectui#3501 的 `PLATFORM_RECORD_COLUMNS`。 与既有的 `showcase_zoo_user_gate`(声明的 `user` 字段)成对:注入列与声明字段必须 门控一致。 标签当场补 zh-CN,棘轮仍是 451、`check-i18n-coverage: OK (none new)`; `pnpm validate` exit 0、`typecheck` 通过、146 tests passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 42af12f commit ede5a8e

7 files changed

Lines changed: 761 additions & 3 deletions

File tree

examples/app-showcase/objectstack.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { setupShowcaseExternalDatasource } from './src/system/datasources/extern
2020
import { registerRecalcEndpoint } from './src/system/server/recalc-endpoint.js';
2121
import { registerShowcasePositionBindings } from './src/security/bind-position-sets.js';
2222
import { registerShowcaseApprovalDemo } from './src/security/seed-approval-demo.js';
23-
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews, ContactViews } from './src/ui/views/index.js';
23+
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews, ContactViews, FieldZooViews } from './src/ui/views/index.js';
2424
import { ShowcaseApp } from './src/ui/apps/index.js';
2525
import { ChartGalleryDashboard, OpsDashboard, RevenuePulseDashboard } from './src/ui/dashboards/index.js';
2626
import { ShowcaseTaskDataset, ShowcaseProjectDataset, ShowcaseInvoiceDataset, ShowcaseAccountDataset } from './src/ui/datasets/index.js';
@@ -193,7 +193,7 @@ export default defineStack({
193193

194194
// UI
195195
apps: [ShowcaseApp],
196-
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews, ContactViews],
196+
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews, ContactViews, FieldZooViews],
197197
pages: [CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage],
198198
dashboards: [ChartGalleryDashboard, OpsDashboard, RevenuePulseDashboard],
199199
books: allBooks,

examples/app-showcase/src/security/capabilities.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,27 @@ export const ExportDataCapability = defineCapability({
5656
packageId: 'com.example.showcase',
5757
});
5858

59-
export const allCapabilities = [ExportDataCapability];
59+
/**
60+
* A capability that is DEFINED and granted to NOBODY — the falsifiable half of
61+
* the ADR-0066 three-way separation, and the one a demo usually leaves out.
62+
*
63+
* Its whole job is to be required and never held: `showcase_zoo_perm_missing`
64+
* (and the AND-gate specimen next to it, see ui/actions/predicate-matrix.action.ts)
65+
* lists it in `requiredPermissions`, so those buttons must be absent for every
66+
* caller on every one of the four action surfaces. Without a capability nobody
67+
* holds there is nothing to notice when a surface stops applying the gate —
68+
* which is exactly how the selection bar shipped ignoring `requiredPermissions`
69+
* outright (objectui#3492) while three other surfaces honoured it.
70+
*
71+
* Deliberately absent from every permission set. If a future set grants it, the
72+
* two specimens stop testing anything.
73+
*/
74+
export const RestrictedOpsCapability = defineCapability({
75+
name: 'showcase.restricted_ops',
76+
label: 'Restricted Showcase Operations',
77+
description: 'Reserved for the capability-gate specimens — intentionally granted to no permission set.',
78+
scope: 'org',
79+
packageId: 'com.example.showcase',
80+
});
81+
82+
export const allCapabilities = [ExportDataCapability, RestrictedOpsCapability];

examples/app-showcase/src/system/translations/index.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,13 +419,28 @@ export const ShowcaseTranslationBundle = {
419419
showcase_field_zoo: {
420420
label: '字段动物园', pluralLabel: '字段动物园',
421421
fields: { f_lookups: { label: '查找 → 客户(多值)' } },
422+
// 三个视图共用一套门控动作,差别只在「被门控的字段是否也作为列显示」——
423+
// 即客户端是否展开它、投影是否本来就会带上它。见 ui/views/field-zoo.view.ts。
424+
// `_views.default` —— 容器的默认列表:它声明时不带 `name`,composer 把它
425+
// 注册为 `showcase_field_zoo.default`,bundle 的键就是这个裸运行时键
426+
// (#5164,2026-08-06 定案)。写成 `list` 是没有任何查找能到达的拼法 ——
427+
// 本条正是这么错过一次的:分支落后 main 时本地绿、合进 main 后 CI 红。
428+
_views: {
429+
default: { label: '字段动物园' },
430+
gated_columns: { label: '被门控字段作为列' },
431+
inline_bulk_defs: { label: '内联批量定义' },
432+
},
422433
// #3405 — the inline system-object picker specimen. Translated at birth
423434
// because `check-i18n-coverage` ratchets this example at its current
424435
// untranslated count: a newly declared label that skips zh-CN pushes the
425436
// count up and fails the gate. (The gallery's older params sit inside
426437
// that baseline; this one is not allowed to widen it.) `负责人` matches
427438
// what the bundle already renders for `showcase_task.assignee`, rather
428439
// than introducing a second word for the same idea.
440+
// 动作显隐矩阵(objectui#3492 / #3501)。同上:本示例被
441+
// `check-i18n-coverage` 按当前未翻译计数上了棘轮,新声明的标签必须当场
442+
// 翻译,否则计数上涨、门变红。标签本身是覆盖夹具的说明文字,中文照写同
443+
// 一句技术陈述,不做意译。
429444
_actions: {
430445
showcase_action_param_gallery: {
431446
params: {
@@ -435,6 +450,65 @@ export const ShowcaseTranslationBundle = {
435450
},
436451
},
437452
},
453+
showcase_zoo_relation_gate: { label: '查找 == 多值第一项', successMessage: '两个关系都解析到同一个 id。' },
454+
showcase_zoo_user_gate: { label: '指派给我(user 字段)', successMessage: '你是这条标本的指派用户。' },
455+
showcase_zoo_owner_gate: { label: '归我所有(owner_id)', successMessage: '这条标本归你所有。' },
456+
showcase_zoo_dialect_split: { label: '仅 CEL:contains()' },
457+
showcase_zoo_toolbar_gate: { label: '仅登录可见(工具栏)', successMessage: '工具栏门通过。' },
458+
showcase_zoo_visible_string: { label: 'visible:字符串' },
459+
showcase_zoo_visible_tagged: { label: 'visible:P`…` 标签模板' },
460+
showcase_zoo_visible_envelope: { label: 'visible:{ dialect, source } 信封' },
461+
showcase_zoo_disabled_gate: { label: '未评分则禁用' },
462+
showcase_zoo_perm_held: { label: '需要导出能力' },
463+
showcase_zoo_perm_missing: { label: '需要受限能力' },
464+
showcase_zoo_perm_and: { label: '需要同时具备两项能力' },
465+
showcase_zoo_perm_empty: { label: 'requiredPermissions:[]' },
466+
// 字段类型谓词园 —— 每种字段类型一条 `visible`。
467+
showcase_zoo_t_lookup: { label: 'lookup —— 已设置' },
468+
showcase_zoo_t_lookup_multi: { label: 'lookup 多值 —— 多于 1 项' },
469+
showcase_zoo_t_master_detail: { label: 'master_detail —— 已设置' },
470+
showcase_zoo_t_tree: { label: 'tree —— 未设置' },
471+
showcase_zoo_t_user: { label: 'user —— 未设置' },
472+
showcase_zoo_t_user_identity: { label: 'user —— 是我' },
473+
showcase_zoo_t_owner: { label: 'owner_id(注入列)—— 属于我' },
474+
showcase_zoo_t_text: { label: 'text —— name 非空' },
475+
showcase_zoo_t_textarea: { label: 'textarea —— contains' },
476+
showcase_zoo_t_email: { label: 'email —— matches' },
477+
showcase_zoo_t_url: { label: 'url —— contains' },
478+
showcase_zoo_t_phone: { label: 'phone —— contains' },
479+
showcase_zoo_t_markdown: { label: 'markdown —— contains' },
480+
showcase_zoo_t_html: { label: 'html —— contains' },
481+
showcase_zoo_t_code: { label: 'code —— contains' },
482+
showcase_zoo_t_number: { label: 'number —— > 100' },
483+
showcase_zoo_t_currency: { label: 'currency —— > 1000' },
484+
showcase_zoo_t_percent: { label: 'percent —— >= 75' },
485+
showcase_zoo_t_rating: { label: 'rating —— >= 4' },
486+
showcase_zoo_t_slider: { label: 'slider —— > 50' },
487+
showcase_zoo_t_progress: { label: 'progress —— >= 80' },
488+
showcase_zoo_t_formula: { label: 'formula —— > 100' },
489+
showcase_zoo_t_autonumber: { label: 'autonumber —— 等于 0001' },
490+
showcase_zoo_t_date: { label: 'date —— 早于今天' },
491+
showcase_zoo_t_datetime: { label: 'datetime —— 已设置' },
492+
showcase_zoo_t_time: { label: 'time —— 等于 14:30' },
493+
showcase_zoo_t_boolean: { label: 'boolean —— 真' },
494+
showcase_zoo_t_toggle: { label: 'toggle —— 真' },
495+
showcase_zoo_t_select: { label: 'select —— high' },
496+
showcase_zoo_t_radio: { label: 'radio —— yes' },
497+
showcase_zoo_t_multiselect: { label: 'multiselect —— 含 red' },
498+
showcase_zoo_t_checkboxes: { label: 'checkboxes —— 含 email' },
499+
showcase_zoo_t_tags: { label: 'tags —— 非空' },
500+
showcase_zoo_t_json: { label: 'json —— 嵌套键' },
501+
showcase_zoo_t_location: { label: 'location —— 纬度 > 40' },
502+
showcase_zoo_t_address: { label: 'address —— 美国' },
503+
showcase_zoo_t_color: { label: 'color —— 等于 #2563EB' },
504+
showcase_zoo_t_composite: { label: 'composite —— width 为 10' },
505+
showcase_zoo_t_repeater: { label: 'repeater —— 2 行' },
506+
showcase_zoo_t_record: { label: 'record 套 record —— score 为 9' },
507+
showcase_zoo_t_vector: { label: 'vector —— 4 维' },
508+
showcase_zoo_t_and: { label: 'AND —— boolean && number' },
509+
showcase_zoo_t_or: { label: 'OR —— select || rating' },
510+
showcase_zoo_t_not: { label: 'NOT —— !boolean' },
511+
showcase_zoo_t_ternary: { label: '三元 —— rating ? :' },
438512
},
439513
},
440514
},

examples/app-showcase/src/ui/actions/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
import { defineAction } from '@objectstack/spec/ui';
44

5+
// The action-GATING matrix (`visible` / `requiredPermissions` across all four
6+
// action surfaces, and one predicate per field type) lives in its own file —
7+
// it is a coverage fixture for the gates, not another ActionType specimen.
8+
import { allPredicateMatrixActions } from './predicate-matrix.action.js';
9+
10+
export * from './predicate-matrix.action.js';
11+
512
const task = 'showcase_task';
613
const invoice = 'showcase_invoice';
714
const fieldZoo = 'showcase_field_zoo';
@@ -375,4 +382,5 @@ export const allActions = [
375382
ActionParamGalleryAction,
376383
ArchiveTaskAction,
377384
PortfolioSnapshotAction,
385+
...allPredicateMatrixActions,
378386
];

0 commit comments

Comments
 (0)