feat(spec): element:button declares the action it executes — InlineActionSchema (objectui#2997) - #4135
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 30, 2026
Closed
xuyushun441-sys
force-pushed
the
claude/inline-action-2997
branch
from
July 30, 2026 11:02
6ee4407 to
20f6582
Compare
…tionSchema (objectui#2997)
element:button's renderer reads `properties.action` and dispatches it
through the ActionRunner — it is the only thing making a standalone-page
button interactive — and ElementButtonPropsSchema did not declare it.
It is authored anyway: cloud's service-tenant pages carry five sites across
the billing and pricing funnel, each `{type:'navigation', to: …}`, each cast
`as any`.
An undeclared prop is STRIPPED, not ignored — safeParse returned success
with no `action` in `data`. Harmless only because page block `properties`
are still z.record(z.string(), z.unknown()); the moment that is tightened
to validate against ComponentPropsMap, every one of those buttons loses its
action at save with a green parse.
InlineActionSchema is derived, not a new dialect. ActionSchema is
z.object(…).refine(…).refine(…), so `.pick()` is unavailable on the export;
the object half is now a factory both build from, preserving lazySchema's
deferral. The inline schema picks the twelve fields element:button actually
forwards, sharing their describe() text, the ActionType vocabulary and the
target-required refinement.
`name`/`label` are optional — a registry entry needs an identity and a menu
label, an inline action has neither (the button supplies its own label).
Registry-only concerns are excluded, as are icon/variant (the button has its
own) and body (a page button running a sandboxed script is a separate
decision).
`navigation` and `to` become normalizing aliases folding to `url`/`target`,
the VIEW_FILTER_OPERATOR_ALIASES pattern: cloud's pages keep validating
while output is always canonical, and the aliases can be tombstoned once
producers migrate. `url` is also the better target — its runner path has
${param} interpolation, apiBase promotion and popup-safe openIn; the
navigation path has none of those.
NOT done: promoting `navigation` into ActionType bare (declined in #4070) —
it names the type while leaving the prop undeclared and `to` homeless, and
adds a permanent synonym to a vocabulary whose members cannot be removed.
Nothing narrowed; all generated surfaces gain entries only. spec suite
7038/271 green, tsc clean, all twelve check:* gates pass.
Refs objectstack-ai/objectui#2997, objectstack-ai/objectui#2945, #4070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xuyushun441-sys
force-pushed
the
claude/inline-action-2997
branch
from
July 30, 2026 11:07
20f6582 to
25be818
Compare
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage A of objectstack-ai/objectui#2997. Framework-only and carries no migration — nothing that parses today stops parsing.
The gap
element:button's renderer readsproperties.actionand dispatches it through theActionRunner(packages/components/src/renderers/basic/elements.tsx:212-240). That prop is the only thing making a standalone-page button interactive.ElementButtonPropsSchemadeclaredlabel,variant,size,icon,iconPosition,disabled,aria— and noaction.It is being authored regardless. cloud
main,packages/service-tenant/src/pages/:pricing.page.ts:42const goToEnvironments = { type: 'navigation', to: ENVIRONMENTS_ROUTE }welcome.page.ts:66const go = (to) => ({ type: 'navigation', to })— used 4×billing-cancel.page.ts:66, :76{ type: 'navigation', to: … }billing-success.page.ts:74{ type: 'navigation', to: … }Five declaration sites, ~9 rendered buttons, the billing and pricing funnel. Every one cast
as any.An undeclared prop is stripped, not ignored
No
action. Non-strictz.object, so Zod drops the unknown key.Harmless today only because page block
propertiesarez.record(z.string(), z.unknown())(page.zod.ts:86), so this schema never runs on a real page save. The moment anyone tightenspropertiesto validate againstComponentPropsMap— an obvious future hardening — every one of those cloud buttons silently loses its action at save, with a green parse. Declaring the prop is what defuses that; nothing else does.InlineActionSchemais derived, not a fourth dialectActionSchemaisz.object(…).refine(…).refine(…), so.pick()is not available on the exported schema — aZodObjectmethod behind two refinement wrappers. (Worth recording: this, not thelazySchemaproxy, is why subsetting failed. The proxy forwards everything, which is whyChartTypeSchema.extract(…)works — that one wraps a barez.enum.)So the object half is now a factory both schemas build from:
A factory rather than a module-scope schema so
lazySchema's whole point survives — the fields are constructed on first use of whichever schema is touched, not at module load.The twelve picked fields are exactly what
element:buttonforwards to the runner, so theirdescribe()text, theActionTypevocabulary and thetarget-required refinement are shared, not restated.nameandlabelare optional here — the substantive difference.ActionSchemarequires both because a registry entry needs an identity and a menu label; an inline action has neither, and the button already has its ownlabelprop (requiringaction.labeltoo would mean writing it twice — the reasonaction: ActionSchemawas the wrong shape).Excluded on purpose: every registry-only concern (
objectName,locations,order,ai,requiredPermissions,visible/disabled,resultDialog), plusicon/variant(the button has its own) andbody(a page button running an inline sandboxed script is a separate decision). A declared field no renderer reads is the failure this schema exists to stop — widen it when a renderer widens, not before.navigationandtoare normalizing aliases, with an end statenormalizeInlineActionfoldstype: 'navigation'→'url'andto→targeton parse — theVIEW_FILTER_OPERATOR_ALIASESpattern. cloud's pages keep validating unchanged, parse output is always canonical, and the aliases get aretiredKeytombstone once the producers migrate.urlis also the better target, not merely the canonical one: the runner'surlpath has${param.X}/${ctx.X}interpolation,apiBasepromotion for/api/…paths and popup-blocker-safeopenIn. Itsnavigationpath has none of those and adds onlyreplace— which no producer writes (checked: noreplace,newTab,externalornavigateanywhere in cloud).Deliberately not done: promoting
navigationintoActionTypeas a bare member, which was considered and declined in #4070. It would name the type while leaving the prop undeclared andtohomeless — half a fix — and add a permanent synonym to a vocabulary whose members cannot be removed later.Verification
packages/spec/src/ui/inline-action.test.ts). The derivation is asserted from both directions: the inline field set is exactly the documented twelve; every one of them round-trips throughActionSchema(so a rename there fails here); every registry-only concern is absent; everyActionTypemember is inline-authorable, so a new action type needs no second edit. The fold is pinned against the literal shape cloud writes, and against the negative cases — a canonical body passes through by identity, an explicittargetwins overto,tois never emitted back out, non-objects are untouched.InlineActionSchemaaccepts exactly whatActionSchemarefuses ({type:'url', target}→ the latter reportsnameandlabelmissing), so the two cannot silently converge.@objectstack/specsuite: 7038 tests across 271 files, green.tsc --noEmit: clean.check:*gates pass, and every generated surface gains entries only — zero removals acrossapi-surface.json,authorable-surface.jsonandjson-schema.manifest.json.What follows
{ type: 'url', target }and drop theas any. Note objectstack#4107's normalization graft does not reach them — those are code-defined*.page.ts, notsaveMeta-persisted.actiontoblock-config.ts'selement:buttonentry, or the Studio inspector still cannot author it (that table is hand-written, not derived from these props).to, drop thenavigationalias.Refs objectstack-ai/objectui#2997, objectstack-ai/objectui#2945, #4070, #4107
🤖 Generated with Claude Code