Skip to content

feat(list): unify conditional formatting + row-action visibility onto the CEL engine (#1584)#2544

Draft
os-zhuang wants to merge 1 commit into
mainfrom
claude/unify-listview-cel-conditional-s992ms
Draft

feat(list): unify conditional formatting + row-action visibility onto the CEL engine (#1584)#2544
os-zhuang wants to merge 1 commit into
mainfrom
claude/unify-listview-cel-conditional-s992ms

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

Closes #1584. Converges the list-view conditional tier — conditional formatting (list / grid / kanban) and row-action visible / disabled — onto @objectstack/formula's CEL engine, the same engine the server uses (framework ADR-0058). Previously these evaluated on the legacy JS-dialect ExpressionEvaluator, even though @objectstack/spec already types them as CEL (ListViewSchema.conditionalFormatting[].condition and ActionSchema.visible are ExpressionInputSchema). This was a spec-compliance gap: toPredicateInput unwrapped a { dialect: 'cel', source } envelope and fed it to the wrong dialect. Now the whole platform speaks one expression dialect.

What changed

  • @object-ui/core — new zero-React helpers next to evalFieldPredicate:
    • evalRowPredicate(pred, row, opts) — one boolean predicate over a row on the canonical CEL engine; binds record.*, bare fields, data.*, and the host predicate scope (features / user).
    • resolveConditionalFormatting(record, rules, scope) — the single implementation of all three historical rule shapes.
    • Dialect routing: a { dialect: 'cel' } envelope is always CEL; a bare string is CEL per the spec contract, unless it carries legacy-only syntax (${…}, ===/!==, ?., .includes()) → routed to the legacy engine with a one-time deprecation warning. The native { field, operator, value } form is translated to CEL.
  • @object-ui/react — new useRowPredicate hook (canonical CEL + ambient predicate scope).
  • Consumers converged: ListView.evaluateConditionalFormatting is now a thin wrapper (export kept); ObjectGrid.getRowStyle's inline copy deleted; kanban card styles use the shared helper; grid + data-table row-action menus switch to the CEL path.
  • Bug fix: plugin-view's ObjectView kanban branch now forwards top-level conditionalFormatting (it was silently dropped for kanban).
  • Failure posture (per ADR-0058's fail-policy matrix): formatting failure → no style + warn; row-action visible fails closed (hidden + warn, aligns with ActionEngine); disabled fails soft (not disabled + warn). Nothing silent.
  • Legacy form condition { field, equals/notEquals/in } retired to a CEL translation at the form.tsx entry (back-compat preserved); the two FieldDesigner authoring sites migrated to visibleWhen.

Capability gain

The CEL in operator and list membership (record.status in ['sent','paid']) now work in row predicates — the legacy parser had no in operator and silently failed open on them.

Back-compat

Near-zero blast radius (full-tree survey found no real authored usages of these features; only package-test fixtures). Existing conditional-formatting rules, row-action predicates, and form condition metadata keep working via translation / dialect routing (with deprecation warnings on legacy-dialect strings).

Naming (settled by ADR-0089)

Actions keep visible (ADR-0089 renamed only field/view/page keys; action.zod.ts keeps visible, already CEL). Spec { condition, style } stays canonical for formatting; ObjectUI-local shapes are back-compat translations. useCondition / useExpression (schema/widget ${} tier) are untouched — only row/record predicate call-sites moved.

Tests

  • packages/core/.../listConditional.test.ts — 19 tests: three rule shapes, dialect routing, in, legacy fallback + warn, fail-closed + warn, missing-key.
  • packages/react/.../useExpression.test.tsuseRowPredicate: boolean short-circuit, scope merge, CEL in, fail-closed.
  • packages/plugin-grid/.../RowActionMenu.test.tsx — CEL in show/hide end-to-end on the real menu.
  • packages/components/.../form-legacy-condition.test.tsx — legacy condition → CEL (equals / notEquals / in), reactive.
  • e2e/live/list-row-action-cel.spec.ts — live spec guarding the row-action-menu render path against the showcase task list's visible: '!record.done' action.

All touched-package suites pass (core + react 1327; grid + kanban 216; components/list/view/designer 508). turbo type-check green across all touched packages. No new lint errors.

Notes / follow-ups

  • Live e2e requires the running stack (backend + console dev server) and, for conditional formatting, showcase metadata that declares it (a framework change the issue scopes out). The added live spec targets the existing showcase row-action visible predicate; the gate semantics are covered deterministically by the jsdom component tests. The live stack isn't runnable in the authoring sandbox.
  • Perf: @objectstack/formula's celEngine.evaluate re-parses per call (cel-js exposes no AST re-execution). Acceptable at page-size row counts; worth an upstream program cache in @objectstack/formula if it shows up hot.

🤖 Generated with Claude Code


Generated by Claude Code

… the CEL engine (#1584)

Converge the list-view conditional tier — conditional formatting (list / grid /
kanban) and row-action `visible` / `disabled` — onto @objectstack/formula's CEL
engine, matching how @objectstack/spec already types these surfaces
(ExpressionInputSchema / CEL) and how the server evaluates them (framework
ADR-0058). The whole platform now speaks one expression dialect.

- @object-ui/core: new evalRowPredicate + resolveConditionalFormatting helpers
  (one implementation of the three formatting rule shapes; dialect routing —
  a { dialect: 'cel' } envelope is always CEL, a bare string is CEL unless it
  carries legacy-only syntax which routes to the old engine with a one-time
  deprecation warning; native { field, operator, value } translated to CEL).
- @object-ui/react: new useRowPredicate hook (canonical CEL, ambient scope).
- Consumers converged: ListView.evaluateConditionalFormatting (thin wrapper),
  ObjectGrid row styling (inline copy removed), kanban card styles, grid and
  data-table row-action menus. plugin-view now forwards top-level
  conditionalFormatting to the kanban branch (previously dropped).
- Row-action visible fails closed (broken -> hidden + warn); disabled fails
  soft. The CEL `in` operator now works in row predicates.
- Legacy FormField.condition {field, equals/notEquals/in} retired to a CEL
  translation (back-compat); FieldDesigner migrated to visibleWhen.

Unit + jsdom component tests; docs/skill updated; live e2e spec added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LwrWThBJgvcfPtqNHpstB
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 15, 2026 4:31pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.6 KB 350 KB
Entry file index-Cx5z0F4T.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.17KB 2.95KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 21.70KB 4.21KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.12KB 5.00KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.63KB 2.15KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 30.17KB 7.28KB
auth (createAuthenticatedFetch.js) 3.93KB 1.55KB
auth (index.js) 1.83KB 0.79KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.86KB 0.85KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.42KB 0.96KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 444.81KB 96.19KB
core (index.js) 1.69KB 0.60KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 113.70KB 28.15KB
fields (index.js) 202.59KB 49.53KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 21.22KB 4.69KB
i18n (useSafeTranslation.js) 2.68KB 0.98KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 5.90KB 2.15KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.46KB 1.03KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.50KB 0.70KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.12KB 12.34KB
plugin-charts (index.js) 46.31KB 13.06KB
plugin-chatbot (index.js) 176.76KB 42.28KB
plugin-dashboard (index.js) 108.35KB 26.87KB
plugin-designer (index.js) 213.89KB 43.03KB
plugin-detail (index.js) 210.47KB 50.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 104.26KB 25.24KB
plugin-gantt (index.js) 149.17KB 37.17KB
plugin-grid (index.js) 171.50KB 45.28KB
plugin-kanban (index.js) 47.17KB 12.85KB
plugin-list (index.js) 97.92KB 23.14KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.82KB 9.96KB
plugin-timeline (index.js) 25.37KB 7.20KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.69KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 0.79KB 0.43KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
tenant (TenantContext.js) 0.31KB 0.25KB
tenant (TenantGuard.js) 1.04KB 0.43KB
tenant (TenantProvider.js) 2.76KB 0.98KB
tenant (TenantScopedQuery.js) 0.77KB 0.44KB
tenant (index.js) 0.75KB 0.38KB
tenant (resolver.js) 2.64KB 0.76KB
tenant (useTenant.js) 0.50KB 0.32KB
tenant (useTenantBranding.js) 0.62KB 0.39KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.54KB 0.68KB
types (layout.js) 0.20KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.26KB 1.96KB
types (tenant.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify list-view conditional features (conditional formatting / row-action visibility) onto the CEL engine

2 participants