feat(app-shell): CEL lint + field autocomplete for condition predicates (#1582)#2567
Merged
Conversation
…es (#1582) ConditionBuilder's raw-expression escape hatch — a bare <textarea> — is replaced by CelPredicateField, so every surface that authors a condition through it gains inline syntax/semantic validation and field-name autocomplete on the canonical @objectstack/formula engine: - field-level visibleWhen / readonlyWhen / requiredWhen (SchemaForm's condition widget auto-maps /When$/ properties), - action visible / disabled (ActionDefaultInspector), - every other condition-widget property (visibleOn, predicate, ...). The no-code [subject][op][value] builder path is unchanged; only the "Expression" mode is upgraded. An invalid predicate now surfaces a readable inline error instead of failing silently at runtime. The sibling-field catalog reaches the editor via the existing widgetContext.objectFields plumbing (ResourceEditPage fetches it for the record's bound object). EN + ZH labels. Completes the objectui side of #1582. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LwrWThBJgvcfPtqNHpstB
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 16, 2026 05:32
os-zhuang
added a commit
that referenced
this pull request
Jul 16, 2026
…e-2358-3b099c) (#2608) * remove(react): #2545 drop unused FormRenderer/FieldFactory duplicate form path (#2560) Phase 4 of #2545. FormRenderer (and its captive FieldFactory) was an exported-but-dead second form render path with zero runtime consumers — the only import in the whole repo was its own test file. It duplicated plugin-form's ObjectForm (the path every app actually uses via the component registry) as a degraded variant: raw-HTML/Tailwind instead of shared UI primitives, a hard-coded Submit button, no submitBehavior/aria/groups support. - delete packages/react/src/components/form/ entirely (FormRenderer, FieldFactory, their test, README, index) and the now-empty re-export `export * from './components/form'` in packages/react/src/index.ts - remove the public exports FormRenderer/FormRendererProps/FieldFactory/ FieldFactoryProps/ExtendedFormField from @object-ui/react (changeset: minor) - docs: delete content/docs/core/form-renderer.mdx, drop it from core meta.json nav, repoint the schema-renderer and enhanced-actions cross-links (the latter to the live /docs/components/form page), delete the stale example packages/types/examples/form-renderer-example.ts - fix stale guidance that listed FormRenderer as an app-shell export (app-shell never exported it): app-shell/README.md, three skills guides; the plugin-development guide's registration example now shows the real ObjectFormRenderer symbol Verified: pnpm react build clean; vitest react 23 files / 295 tests pass; no FormRenderer/FieldFactory/ExtendedFormField references remain outside CHANGELOGs. Refs #2545. Claude-Session: https://claude.ai/code/session_01Ln4gqtxXuymoQmyy4iRM2z Co-authored-by: Claude <noreply@anthropic.com> * test(e2e-live): B3 cascading-options live e2e — client re-filter + server rejection (#2559 / #1583) (#2562) Adds e2e/live/cascading-options.spec.ts, the live-backend counterpart to the no-backend docs e2e (e2e/cascading-options.spec.ts). Against the real console + backend it drives the showcase `showcase_cascade` create form and asserts the dual-side B3 contract: - CLIENT: the child `province`'s offered option set re-filters as the parent `country` changes, and a now-invalid child value clears (cascade-clear). - SERVER: POST /api/v1/data/showcase_cascade rejects an out-of-set option value (400 VALIDATION_FAILED / invalid_option) and accepts the in-set one — client hiding is UX, the server is the boundary. Excluded from the default/CI run (testIgnore '**/live/**'); opt-in via `pnpm test:e2e:live` with the stack up. Requires the framework showcase_cascade fixture (framework #2559). Claude-Session: https://claude.ai/code/session_01S91NyYJURiQTKmF9q3AXxg Co-authored-by: Claude <noreply@anthropic.com> * feat(flow-designer): connector picker lists dispatchable connectors + marks declarative instances (#2563) Point the connector_action connector picker at the runtime registry (GET /api/v1/automation/connectors) so it lists exactly the dispatchable connectors — plugin connectors plus materialized declarative instances — and annotates declarative ones via the descriptor's origin field. Studio-side follow-up to framework ADR-0096. * fix(detail): gate related lists on the current user's child-object read permission (#2359) (#2565) Related-list sections were derived purely from the relationship graph: every master_detail/lookup FK produced a section (header + grid + New button) even when the current user has no read access to the child object — the grid then showed 'no records' (fetch 403'd) and New 403'd on save. Data access was always enforced server-side; this closes the UI/DX gap. - deriveRelatedLists gains an optional canRead predicate; children the user cannot read are dropped before sections/tabs/reference-rail are synthesized. - RecordDetailView wires the predicate from usePermissions().can once permissions are loaded (fail-open while loading / with no provider — standalone embeds and Studio keep rendering). - record:related_list renderer additionally self-gates on can(objectName, 'read') so hand-authored pages get the same baseline without an explicit requiredPermissions opt-in. * remove(tenant): drop zero-consumer @object-ui/tenant package + types/tenant.ts mirror (#2564) (#2566) Implements the #2564 disposition (owner-approved, enforce-or-remove — same doctrine as framework#2763/framework#2962): the client-side tenancy layer never had a consumer, and its TenantConfig.isolation strategy enum was the UI mirror of the spec's removed tenancy.strategy. Real tenant scoping is server-enforced (X-Tenant-ID via createAuthenticatedFetch + row-level isolation); per-tenant branding is a ThemeSchema concern. - delete packages/tenant and packages/types/src/tenant.ts; drop the tenant type family from the @object-ui/types barrel - retire the advertising: skills guides, eval item 3, content architecture-overview, ROADMAP row - unwire config: changesets fixed group, root vitest alias, console vite alias, lockfile - changeset: @object-ui/types minor (fixed group bumps together) * feat(app-shell): CEL lint + field autocomplete for condition predicates (#1582) (#2567) ConditionBuilder's raw-expression escape hatch — a bare <textarea> — is replaced by CelPredicateField, so every surface that authors a condition through it gains inline syntax/semantic validation and field-name autocomplete on the canonical @objectstack/formula engine: - field-level visibleWhen / readonlyWhen / requiredWhen (SchemaForm's condition widget auto-maps /When$/ properties), - action visible / disabled (ActionDefaultInspector), - every other condition-widget property (visibleOn, predicate, ...). The no-code [subject][op][value] builder path is unchanged; only the "Expression" mode is upgraded. An invalid predicate now surfaces a readable inline error instead of failing silently at runtime. The sibling-field catalog reaches the editor via the existing widgetContext.objectFields plumbing (ResourceEditPage fetches it for the record's bound object). EN + ZH labels. Completes the objectui side of #1582. Claude-Session: https://claude.ai/code/session_019LwrWThBJgvcfPtqNHpstB Co-authored-by: Claude <noreply@anthropic.com> * fix(app-shell): bind current_user.positions into the client predicate scope; align role-gating examples (#1583 / ADR-0058) (#2573) * test(e2e-live): B3 cascading-options live e2e — client re-filter + server rejection (#2559 / #1583) Adds e2e/live/cascading-options.spec.ts, the live-backend counterpart to the no-backend docs e2e (e2e/cascading-options.spec.ts). Against the real console + backend it drives the showcase `showcase_cascade` create form and asserts the dual-side B3 contract: - CLIENT: the child `province`'s offered option set re-filters as the parent `country` changes, and a now-invalid child value clears (cascade-clear). - SERVER: POST /api/v1/data/showcase_cascade rejects an out-of-set option value (400 VALIDATION_FAILED / invalid_option) and accepts the in-set one — client hiding is UX, the server is the boundary. Excluded from the default/CI run (testIgnore '**/live/**'); opt-in via `pnpm test:e2e:live` with the stack up. Requires the framework showcase_cascade fixture (framework #2559). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S91NyYJURiQTKmF9q3AXxg * fix(app-shell): bind current_user.positions into the client predicate scope; align role-gating examples (#1583 / ADR-0058) The console forwarded only id/name/email/role/roles/isPlatformAdmin into the expression scope, so a position-gated option predicate (`'admin' in current_user.positions` — the shape the framework rule-validator binds and enforces, EvalUser) faulted client-side and FAILED OPEN: the gated option stayed visible to everyone and was only rejected by the server on submit. Forward `positions` (auth payload already carries it) in AppContent and RecordFormPage so client hiding and the server verdict agree. Also switch every role-gating example from `current_user.roles` — a key the server never binds, so a predicate written that way is never enforced — to `current_user.positions`: schema-catalog role-gated-options.json, fields/select docs, the skills guide, ADR-0058 example spellings, TSDoc on SelectOption.visibleWhen, evaluator header comments, and the three unit tests' mock scopes. Matches @objectstack/spec's own SelectOption.visibleWhen example. Verified: optionRules/optionLint/SelectField.cascade/useExpression 51/51, schema-catalog 836/836 (incl. the option-predicate guard), eslint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S91NyYJURiQTKmF9q3AXxg --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(page-header): metadata-driven multi-button record header (#2361) (#2574) The record detail page header hardcoded INLINE_MAX=1 — only one action could ever render as an inline button; everything else collapsed into the ⋯ overflow menu, so multi-action objects (Lead: Convert / Assign / Return) could not surface their common operations side by side. The header now renders up to maxVisible inline buttons (default 3 desktop / 1 mobile, overridable via maxVisible / mobileMaxVisible on the page:header schema) and decides which actions claim the inline slots from metadata, mirroring action:bar's #2339 contract: - order ascending (unset = 0; lower = more prominent), stable sort - variant: 'primary' as a tie-break within equal order (and mapped to the Shadcn default Button variant instead of leaking through) - component: 'action:menu' pins an action inside the ⋯ menu regardless of the action count The synthesized system actions declare their placement accordingly: sys_edit gets order: 100 (behind every authored business action, still inline when slots remain); sys_share / sys_delete are pinned into the ⋯ menu via component: 'action:menu' so Delete never surfaces as an inline red button just because an object has few actions. Closes #2361 Claude-Session: https://claude.ai/code/session_01AvDRpozQS6m3UaWpGBtdG2 Co-authored-by: Claude <noreply@anthropic.com> * feat(dashboard): dashboard-level filters driving multiple charts (framework#2501) (#2576) Wire the (previously declared-but-inert) DashboardSchema.globalFilters + dateRange into the runtime: filter values live as dashboard-level variables (PageVariablesProvider, readable as page.<name> in widget expressions), a DashboardFilterBar renders above the widgets, and DashboardRenderer broadcasts the active values into every bound widget's inline query — AND-merged with the widget's own filter (dataset widgets via runtimeFilter). Each widget maps a filter to ITS OWN field via the new DashboardWidgetSchema.filterBindings (string override / false opt-out), defaulting to the filter's own field; legacy targetWidgets allow-list kept. globalFilters[].name added as the stable variable key. Both pending paired @objectstack/spec alignment (framework#2501). - core: pure dashboard-filters module (defs resolution, condition build, per-widget binding resolution); mergeFilters lifted from plugin-report. - Date presets emit date-macro tokens resolved at query time. - Example: schema-catalog plugin-dashboard/filtered-dashboard (2 charts over different objects + opt-out metric). Docs: README + plugin-dashboard.mdx. Tests: 16 core unit tests (binding/condition/merge), 5 renderer tests (broadcast, per-widget fields, opt-out, live re-scope, dataset runtimeFilter). Claude-Session: https://claude.ai/code/session_01U25wX5Ja3oS4dMyVHMnRsK Co-authored-by: Claude <noreply@anthropic.com> * fix(detail+fields+app-shell): ADR-0085 #2548 follow-ups — strip title dedupe, group icon/description, currency channel, approvals Bearer (#2577) UX follow-ups from the framework#2548 real-backend browser pass, re-verified live (14/14): highlight strip drops the record title (new resolveTitleField, both synth + RecordDetailView paths); fieldGroups icon/description reach detail sections (LazyIcon rendering); currencyConfig passes through field enrichment so spec-authored currency fields render their symbol; RecordMetaFooter actor-less fallback labels (+ zh mistranslation fix); select badges ellipsize with hover title; all approvals fetches (app-shell badge/inbox/record panel + console approvalsApi) send the Bearer token for split-origin deployments. * feat(dashboard-filters): #2578 follow-ups — catalog examples, guide tutorial, i18n entries, spec-alignment cleanup (#2581) Follow-ups to the dashboard-level filters feature (#2576, framework#2501): - schema-catalog: five new plugin-dashboard examples — optionsFrom dynamic options, text/number/lookup filter types, dataset + inline widget mix, legacy targetWidgets allow-list, and date presets + custom range with a dateRange opt-out. - docs: new 'Dashboard-Level Filters' guide page (full tutorial from zero: dateRange + globalFilters + filterBindings, page.* expression usage, filter-type condition table, known limitations with workarounds); cross- linked from plugin-dashboard.mdx. - i18n: dashboard.filters.* entries for en and zh (bar label, All time, Custom…, All, Reset, and the 13 date-range preset labels) — the filter bar previously always rendered the useSafeTranslate English fallbacks. - types: GlobalFilterSchema.name + DashboardWidgetSchema.filterBindings are now landed in @objectstack/spec, so the 'Pending alignment' JSDoc flips to 'Aligned' (no shape changes). Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z Co-authored-by: Claude <noreply@anthropic.com> * fix(app-shell): guard Studio Access pillar against silently discarding unsaved matrix edits (#2588) The Access pillar keys PermissionMatrixEditPage by the selected set (key={current}) and swaps it out entirely for the OWD overview, so any rail click REMOUNTED the matrix and silently dropped unsaved checkbox edits — no confirm, no beforeunload. - PermissionMatrixEditPage now tracks dirty state (JSON snapshot baseline re-anchored on load/save, same approach as ResourceEditPage), reports transitions via a new onDirtyChange prop (reset to false on unmount), and installs the standard beforeunload guard while dirty. - AccessPillar holds the reported dirty flag and gates every rail-driven surface swap (set switch, OWD overview button, the matrix's OWD badge deep-link, and the new-set creator) on the same native confirm the metadata editor's leave guard uses (engine.edit.unsavedLeaveConfirm). Re-clicking the open set stays a no-op and never prompts. - Tests: onDirtyChange contract (clean on load, dirty on edit, clean after save, reset on unmount) + AccessPillar guard integration (cancel keeps edits in place, confirm discards, clean switches and no-op re-clicks never prompt, discard clears the guard). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(detail): highlight strip lookup editor honors ObjectStack `reference` key (#2407) (#2587) The record-highlights strip's inline-edit lookup editor (#2407 P2, PR #2549) only copied `reference_to` from the object schema into the enriched field, but backend object schemas use the ObjectStack-convention `reference` key (e.g. showcase_project.account has "reference": "showcase_account"). Result: the LookupField got no target object, so it showed the placeholder instead of the current value and the picker found no options. The details body already normalizes both keys in DetailSection — mirror that here, and copy `reference_field` for parity. Also affects user-type highlights (e.g. team_members → sys_user). Adds regression tests: a lookup highlight whose schema field carries the `reference` key (and `reference_field`) hands the target through to the LookupField editor, alongside the existing `reference_to` spelling. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(app-shell): visual filterBindings editor in the dashboard widget inspector (#2578) (#2586) When the dashboard declares filters (dateRange / globalFilters), the Studio widget inspector now shows a 'Dashboard filter bindings' section — one row per filter with an Apply toggle (unticked writes filterBindings[name]=false) and a field picker re-targeting the filter to this widget's own field (empty = default: the filter's own field). Rows come from the same resolveDashboardFilterDefs normalization the runtime broadcasts from. Previously bindings were only configurable through raw JSON metadata. en + zh inspector strings; 4 new tests (section gating, row rendering, opt-out round-trip, override reset). Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z Co-authored-by: Claude <noreply@anthropic.com> * feat(fields+form+detail): file/image upload cells in inline line-item grids (#2360) (#2585) Field.file in a master-detail inline grid degraded to a text input (no input[type=file] anywhere on the page, so no way to upload from the grid), and auto-derived subform / related-list columns silently dropped file fields. - fields: new FileCell — compact upload control for grid cells (picker button + removable chips, image thumbnails), sharing the UploadProvider pipeline with FileField via an extracted useFileUploads hook. GridField gains a 'file' column type (accept/multiple carried through) and renders file names in list/readonly modes instead of String(value). - plugin-form: deriveColumns/hydrateColumns no longer exclude file/image/avatar fields — they map to 'file' columns and carry the field's multiple + accept (image fields default to ['image/*']). - plugin-detail: auto-derived related-list columns no longer skip file/image fields — they render via the existing FileCellRenderer / ImageCellRenderer. Closes #2360 Claude-Session: https://claude.ai/code/session_01H5tsBSEhsDZmdcHBvfymYC Co-authored-by: Claude <noreply@anthropic.com> * feat(dashboard-filters): #2578 item-5 enhancements — nested variable merging, metadata-aware default bindings, server-side optionsFrom distinct (#2590) - react: nested PageVariablesProviders merge instead of shadowing wholesale — outer page variables stay readable inside an embedded filtered dashboard, same-named inner definitions shadow deliberately, and writes route to the defining scope (resetVariables stays local). 6 new tests. - core: buildWidgetScopedFilter takes an optional knownFields set; a DEFAULT binding whose field is missing from the widget's object is skipped with a console warning instead of emitting an empty-matching query. Explicit filterBindings strings are always honoured. 3 new tests. - plugin-dashboard: DashboardRenderer feeds knownFields from dataSource.getObjectSchema for inline object widgets (best-effort); optionsFrom resolves distinct option values server-side via a dataset GROUP BY (queryDataset inline draft) with the previous client-side top-200 dedupe as fallback. 3 new tests. - docs: guide page known-limitations section rewritten (two of three lifted); README notes updated. Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z Co-authored-by: Claude <noreply@anthropic.com> * fix(data-objectstack): emit MutationEvents from batchTransaction and bulk so master-detail saves refresh bound views (#2584) Fixes #2582. A ModalForm over an object WITH subforms delegates to MasterDetailForm, which persists the parent + child rows through ONE transactional POST /api/v1/batch (the canBatch path — in the console dataSource.batchTransaction always exists). ObjectStackAdapter.batchTransaction never called emitMutation, so the #2269 invalidation bus (onMutation → notifyDataChanged) heard nothing: the related list and the related-tab count badge kept stale values after a successful create/edit until a full page reload. - batchTransaction: after the transaction commits, emit one MutationEvent per operation (creates take id/record from the index-aligned server echo). A failed batch rolled back entirely and emits nothing. - bulk(): each of the create/delete/update branches now emits one resource-level event, matching the existing bulkUpdate/bulkDelete contract (this covers applyDetail's fallback child-row creation path). - batchTransaction now goes through this.fetchImpl instead of the global fetch, consistent with every other raw HTTP call in the adapter (and injectable in tests). Regression tests: batch create/edit emit per-op events, failed batch emits nothing, op without action defaults to create, bulk create/delete/update emit one resource-level event, zero-row and failed bulk emit nothing. Claude-Session: https://claude.ai/code/session_01CHhyf56Z69KNL6aCAsQfKQ Co-authored-by: Claude <noreply@anthropic.com> * fix(fields+detail): resolve pre-existing rules-of-hooks violations in cell renderers (#2595) Currency/Email/Phone cell renderers called hooks after their empty-value early return (latent hook-count crash when a value flips between null and set); useFieldLabel wrapped useObjectTranslation in try/catch (hook-order desync risk; the hook is provider-safe); ReferenceCellRenderer no longer constructs JSX inside try/catch; RecordMetaFooter UserRef renders the registry cell renderer via React.createElement. No behavior change; react-hooks errors 9 → 0; vitest fields+plugin-detail 556 ✓. * feat(kanban): default lane field honours the ADR-0085 stageField role (#2596) Kanban views without an explicit lane field resolved to a hard-coded 'status' (ObjectView options + ListView fallback); the default now goes explicit config → stageField role → strict-false suppression (no default lanes) → shared name/type heuristic, via detectStatusField moved to @object-ui/types (plugin-detail re-exports unchanged). ListView's schema memo gains the async objectDef dep, and its useListFieldLabel drops the try/catch-around-hook (same class as #2595). Out-of-scope findings recorded: report bucketing has no default-grouping seam; list badges already served by option-color select badges. * feat(studio): CEL editor with validate + autocomplete for field conditional rules (#1582) (#2571) The object designer's field inspector now edits visibleWhen / readonlyWhen / requiredWhen with the CelPredicateField CEL editor instead of a raw string (only conditionalRequired had even that). The editor runs in the new scope='record' mode matching how these rules evaluate at runtime (@object-ui/core evalFieldPredicate binds record/previous/parent only): - lint via @objectstack/formula validateExpression(scope:'record') — a bare field ref is an error with the exact record.<field> fix; record./previous. members are checked against the object's fields with did-you-mean; - autocomplete offers the runtime-bound roots + stdlib bare, and the object's own field catalog as MEMBER completion after record. / previous. (new memberTokenAt helper; empty segment right after the dot surfaces the full catalog); - values round-trip both wire shapes (bare string | {dialect,source} envelope, envelope extras like meta.rationale preserved); the deprecated conditionalRequired alias reads into Required-when and migrates to requiredWhen on first edit; - validateMetadataDraft('object') now lints every field's rules draft-wide, so an invalid predicate on ANY field surfaces under fields.<field>.<rule> in the edit banner before save. Verified in the preview gallery (?only=object): inline errors, record. member completion, Valid CEL affordance. 715 metadata-admin tests green. Closes #1582 Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(studio): stop force-opening the new-object dialog on empty packages (#2569) Visiting an empty writable package's Data pillar auto-opened the "new object" creator dialog on EVERY mount — an unrequested modal that duplicated the guidance the empty-state panel already gives, and re-appeared each time you navigated back (dogfood #2555 item 1). The dialog now stays closed; the empty state carries an explicit "New object" CTA instead (hidden on read-only packages), and the firstObjectHint copy points at the button rather than the rail inputs. DataPillar is exported for the new behavioral test. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(fields): PeoplePicker cursor resets only on real result changes (de-flakes keyboard test) (#2594) The keyboard-cursor reset effect keyed on the records array identity, so a background re-emission of identical results (StrictMode double-effect, refetch-on-focus) reset the active row to none mid-navigation. On slow CI that landed between ArrowDown and Enter, dropping the commit — the flaky "ArrowDown then Enter" failure. Key the reset on the record-id signature instead, and make the test wait for the row to be visibly active before pressing Enter. Claude-Session: https://claude.ai/code/session_01H5tsBSEhsDZmdcHBvfymYC Co-authored-by: Claude <noreply@anthropic.com> * fix(dashboard-filters): spec-form filter options crashed the dashboard; add guide screenshots (#2578) (#2597) Browser dogfood of the showcase Revenue Pulse dashboard caught a crash: GlobalFilterSchema.options in the @objectstack/spec object form ({ value, label } — what the spec validates and framework dashboards ship) was rendered un-normalized as a React child ('Objects are not valid as a React child'), taking down the whole dashboard. The filter bar only supported the objectui bare-string shorthand. - core: resolveDashboardFilterDefs normalizes both shapes to {value,label} pairs (DashboardFilterDef.options typed accordingly); new test covers both forms. - plugin-dashboard: the select consumes normalized pairs and the trigger now shows the selected option's LABEL, not its raw value. - types: GlobalFilterSchema.options aligned with the spec union (string | {value,label}). - docs: guide gains three real screenshots from the live walkthrough (default view, EMEA re-scope with opted-out KPI holding, Studio widget-inspector bindings section) under apps/site/public/img/guide/dashboard-filters/. Verified end-to-end in the browser after the fix: filter bar renders, region/date changes live re-scope invoice AND account widgets against their own fields, Reset appears when dirty, and the Studio inspector edits bindings visually. 1323 tests green across core/plugin-dashboard/ types. Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z Co-authored-by: Claude <noreply@anthropic.com> * fix(core+data-objectstack+app-shell): canonicalize reference/reference_to at the schema chokepoints (#2407) (#2598) Backend object schemas key a relational field's target as `reference` (ObjectStack convention: showcase_project.account → { type: 'lookup', reference: 'showcase_account' }), while ObjectUI types and most consumers read `reference_to`. PR #2587 fixed HeaderHighlight case-by-case; this closes the class of bug at the chokepoints instead. - @object-ui/core: new normalizeFieldReferenceKeys / normalizeSchemaReferenceKeys — stamp BOTH snake_case keys whenever either (or legacy camelCase referenceTo) is present; in-place, idempotent, never overwrites existing keys. - ObjectStackAdapter.getObjectSchema: run the pass on the cached schema (same seam as applyFieldWidgetOverrides) — covers every dataSource.getObjectSchema consumer. - app-shell MetadataProvider: run the pass on `object` items at ingestion — covers every useMetadata().objects consumer. Remaining single-key readers confirmed against the live showcase schema and fixed with the dual-key fallback (they can receive schemas that bypass the chokepoints — mock/api data sources, spec-authored configs): - ObjectGantt quick-filters: lookup/master_detail option domain never loaded for reference-keyed schemas (refObject was undefined). - RecordDetailDrawer: lookup cells lost reference_to, so id→name never resolved in the quick-look drawer. - ReportView column hydration: read referenceTo/reference?.to/target — none of which exist on store object defs; lookup report columns never got their target. - attachInlineSubforms (mirror direction): parent resolved only from `reference`, missing reference_to-keyed defs. - sectionFields spec override (mirror direction): author-supplied reference_to on a form field spec was silently dropped. - metadataConverters.toFieldDefinition: referenceTo now falls back to the served `reference` key. Unit tests cover the utility, the adapter pass, gantt quick-filter options, inline subforms, and the section-field override. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(flow-designer): pick the target node per branch in the Decision Branches editor (#1942) (#2568) The decision Branches editor gains a Target column — a node picker scoped to this flow — wiring each branch to its downstream node in the same table as its label and CEL expression (Salesforce Flow Decision Outcomes style). Completes the per-edge Branch picker (#1930) from the node side. The column is virtual: derived from the decision's out-edges (routing truth) and never stored on config.conditions, so it round-trips with FlowEdgeInspector and canvas rewiring. Picking a target creates/updates/retargets the branch's edge carrying its condition/label/default; clearing detaches the edge, never the node. Commits with no targets anywhere (engine-published configSchema forms) keep the legacy #1927 by-order mirror unchanged. Custom edge guards and fault/back edges are never touched. New pure module flow-decision-edges.ts + unit tests for the reconciliation. Closes #1942 Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(app-shell,core): keep error-envelope objects out of toast.error — React #31 page crash (#2579) (#2580) A failed api/flow/server action returning the ObjectStack envelope `{ error: { code, message } }` rode the error OBJECT through `result.error` into ActionRunner's post-execution toast, and `toast.error(object)` rendered it as a React child — Minified React error #31 inside flushSync took down the whole page (reported from Setup → Create User's 400, objectstack-ai/framework#3031). Two layers so no single handler bug can crash the page again: - sources: shared errorDetail() in useConsoleActionRuntime flattens the envelope to a string (error string → error.message → message → HTTP fallback) across apiHandler / flowHandler / serverActionHandler; - sink: ActionRunner coerces a non-string result.error to its .message (or a generic string) before calling the toast handler. * fix(components): pin sticky leading cells at measured header widths (#2592) The checkbox / row-number / frozen data cells stuck at hardcoded 40px offsets, but the table's auto layout doesn't guarantee those widths (the checkbox column collapses to its ~28px min-content), leaving an uncovered strip between pinned cells where horizontally scrolled content showed through (steedos-labs/os-project-titanwind-ehr#418). Measure the real header-cell widths (ResizeObserver keeps them fresh across column resize / density / content changes) and pin each leading sticky cell at the cumulative measured width of the cells before it, falling back to the previous estimates until the first measurement. * fix(fields): localize relative-date humanize via Intl.RelativeTimeFormat (framework#3040) (#2593) The near-today (±7d) window of date fields rendered hardcoded English (Today/Tomorrow/Yesterday/In N days/Nd ago/Overdue Nd) regardless of the workspace locale. Now: - formatRelativeDate resolves the generic phrases through Intl.RelativeTimeFormat(locale, { numeric: 'auto' }) (sentence-cased), falling back to English literals on an invalid locale tag. - The due-like "Overdue Nd" wording (no Intl equivalent) resolves through the i18n bundle key fields.relativeDate.overdue with an English fallback; the key is added to all 10 built-in locales. - DateCellRenderer feeds the ADR-0053 tenant locale (useLocalization) and the i18n translate fn into formatDate/formatRelativeDate, covering list columns, detail header chips, and detail body alike. - formatDate's default absolute path also honors options.locale. * fix(app-shell): lock the Access pillar permission matrix in read-only packages (#2570) A read-only package's Studio top bar showed the "Read-only" chip while the Access pillar's embedded PermissionMatrixEditPage stayed fully editable with a live Save button: AccessPillar only used its readOnly prop to hide the "New permission set" button and never passed it down, and the editor's internal writable gate (allowOrgOverride) is a TYPE-level concept with no host-side input. - PermissionMatrixEditPage grows a readOnly prop, ANDed into `writable`, so the one existing switch locks everything: object/field checkboxes, bulk buttons, name/label inputs, capability picker, advanced facets, and Save. - The read-only badge now words itself for the gate that tripped: package gate mirrors the top-bar chip (engine.studio.pkg.readonly + hint) so the screen no longer contradicts itself; the type gate keeps the OS_METADATA_WRITABLE wording. - AccessPillar passes readOnly through. - New RTL suite drives the real editor through both gates plus the editable regression path. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(plugin-list): spec bare-string sort form crashed ListView (#2578 shape-mismatch audit) (#2601) * fix(dashboard-filters): spec-form filter options crashed the dashboard; add guide screenshots (#2578) Browser dogfood of the showcase Revenue Pulse dashboard caught a crash: GlobalFilterSchema.options in the @objectstack/spec object form ({ value, label } — what the spec validates and framework dashboards ship) was rendered un-normalized as a React child ('Objects are not valid as a React child'), taking down the whole dashboard. The filter bar only supported the objectui bare-string shorthand. - core: resolveDashboardFilterDefs normalizes both shapes to {value,label} pairs (DashboardFilterDef.options typed accordingly); new test covers both forms. - plugin-dashboard: the select consumes normalized pairs and the trigger now shows the selected option's LABEL, not its raw value. - types: GlobalFilterSchema.options aligned with the spec union (string | {value,label}). - docs: guide gains three real screenshots from the live walkthrough (default view, EMEA re-scope with opted-out KPI holding, Studio widget-inspector bindings section) under apps/site/public/img/guide/dashboard-filters/. Verified end-to-end in the browser after the fix: filter bar renders, region/date changes live re-scope invoice AND account widgets against their own fields, Reset appears when dirty, and the Studio inspector edits bindings visually. 1323 tests green across core/plugin-dashboard/ types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z * fix(plugin-list): spec bare-string sort form crashed ListView (#2578 audit) @objectstack/spec ListViewSchema.sort is string | Array<{field,order}> — the bare-string top-level form ('name desc') hit schema.sort.map and threw 'schema.sort.map is not a function', crashing the list. Found by the spec/renderer shape-mismatch audit following the dashboard filter-options crash. Sort parsing is now one normalized parseSortConfig (exported, unit-tested): bare string, legacy 'field desc' array entries, and {field,order} objects all parse; malformed entries drop instead of throwing. The @object-ui/types declaration already carried the union — only the implementation missed the string branch. Also deduplicates the previously copy-pasted parser between the state initializer and the schema-sync effect. Audit sweep otherwise clean: every other string|object union consumer (gantt quickFilters/tooltipFields, lookupColumns, dependsOn, record-highlights fields, list columns) already normalizes with typeof guards, and spec I18nLabelSchema is plain z.string() so label-as-object crash paths do not exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: 包周涛 <baozhoutao@hotoa.com> Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
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.
Summary
Closes #1582. That issue asks for a proper CEL editor — live validation + field-name autocomplete — on the three field-level
*Wheninputs in Studio's field config form. This delivers it at the right altitude:ConditionBuilder's raw-expression escape hatch (a bare<textarea>) is replaced with the existingCelPredicateField, so every surface that authors a condition through the builder gets the assists, not just the field*Whentrio:visibleWhen/readonlyWhen/requiredWhen— SchemaForm'sdetectConditionWidgetauto-maps/When$/properties to the condition widget;visible/disabled(ActionDefaultInspector);condition-widget property (visibleOn,predicate, …).What changed
ConditionBuilderraw mode now rendersCelPredicateField(inline lint + as-you-type autocomplete, lazily backed by@objectstack/formula'svalidateExpression/introspectScope— the same validators the server uses, per the issue's "reuse, don't reimplement" note). The no-code[subject][op][value]builder path is untouched.ResourceEditPagealready fetchesobjectFieldsfor the record's bound object intowidgetContext→ConditionWidget→ConditionBuilder.fields→fieldNames. No new fetches.Acceptance mapping (#1582)
*Wheninputs validate live and autocomplete field names" — ✅ raw ("Expression") mode lints + autocompletes; the no-code builder remains correct-by-construction with a field dropdown.CelPredicateFieldlint), same UX as the RLS policy editor.Tests
ConditionBuilder.test.tsx(4): raw mode renders the CEL editor and commits edits; a mocked engine lint error surfaces inline; field-name autocomplete offers the object's fields; the no-code builder path is unchanged for simple conditions. Full metadata-admin suite: 89 files / 696 tests green.turbo type-check @object-ui/app-shellgreen. No new lint errors.Notes
celAuthoringbridge is lazy + feature-detected: environments without the formula validators degrade to "no lint / no suggestions", never a broken editor.🤖 Generated with Claude Code
Generated by Claude Code