Skip to content

Commit b3c68c6

Browse files
os-zhuangclaudebaozhoutao
authored
Resolve merge conflicts for #2575 (merge main into claude/github-issue-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/objectstack#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>
1 parent 758559e commit b3c68c6

198 files changed

Lines changed: 7468 additions & 4299 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/fields': patch
4+
'@object-ui/i18n': patch
5+
'@object-ui/app-shell': patch
6+
---
7+
8+
Detail-page UX follow-ups from the ADR-0085 PR4 real-backend browser pass (framework#2548):
9+
10+
- **Highlight strip no longer repeats the record title.** A declared
11+
`highlightFields` list containing the title field rendered it as the first
12+
chip — truncated — directly under the identical page H1. `deriveHighlightFields`
13+
now resolves the title (`primaryField` / `nameField` / deprecated
14+
`displayNameField`, else the conventional display-field names) via the new
15+
exported `resolveTitleField` and filters it from declared lists before the
16+
4-chip cap, matching what the heuristic branch always did. app-shell's
17+
`RecordDetailView` synthParts (which pre-computes the list and bypasses the
18+
derivation) applies the same filter.
19+
- **Per-field currency reaches the renderers.** The spec channel
20+
(`currencyConfig.defaultCurrency`) was dropped by the highlight-strip and
21+
detail-section field enrichment, so a spec-authored currency field could
22+
never show its symbol ("25,000,000" instead of "$25,000,000");
23+
`resolveFieldCurrency` reads it second after the designer-only bare
24+
`currency` key.
25+
- **app-shell approvals fetches send the Bearer token.** The header badge
26+
poll, home-inbox count, and record-page approvals panel were cookie-only
27+
(new shared `bearerAuthHeaders()` util) — same split-origin failure mode as
28+
the console `approvalsApi` fix below.
29+
- **`fieldGroups[].icon` / `description` reach detail pages.** The shared
30+
derivation (ADR-0085 §5) already passed them through; the detail synth
31+
dropped them. Sections now carry both, and `DetailSection` renders a real
32+
Lucide icon for identifier-shaped names (emoji/text values keep the
33+
historical text rendering).
34+
- **Record meta footer stops dangling without an actor.** Seeded/system rows
35+
with `created_by: null` rendered "Created by · 10m ago"; the footer now
36+
falls back to actor-less labels ("Created / Updated"), with new i18n keys in
37+
all six locales (and the zh `createdBy`/`updatedBy` mistranslation fixed:
38+
创建人/更新人, not 创建于/更新于).
39+
- **Select badges ellipsize instead of clipping mid-glyph.** In bounded
40+
containers (highlight-strip columns, grid cells) an overlong option label
41+
used to be cut at the container edge ("Technolog…"); badges now shrink with
42+
an inner truncate and expose the full label as a hover title. The highlight
43+
strip's hover title also prefers the option label over the raw stored value.
44+
45+
Console app (unversioned): `approvalsApi` now sends the stored Bearer token
46+
like every other console call — cookie-only auth silently lost the approvals
47+
surface on split-origin deployments where the SameSite cookie doesn't flow.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@object-ui/app-shell": minor
3+
---
4+
5+
feat(flow-designer): connector picker lists dispatchable connectors + marks declarative instances (ADR-0096)
6+
7+
The `connector_action` node's connector picker read `client.list('connector')`
8+
the declared `connectors:` metadata, which includes inert catalog descriptors and
9+
**misses** plugin-registered connectors. It now reads the runtime registry
10+
(`GET /api/v1/automation/connectors`), i.e. exactly the connectors a
11+
`connector_action` can dispatch: plugin connectors and materialized declarative
12+
instances (framework ADR-0096). Declarative instances are annotated `· declarative`
13+
(from the descriptor's new `origin` field) so authors can tell a materialized
14+
metadata connector apart from a plugin one. Degrades to empty on fetch failure;
15+
the field stays free-text editable. Tolerates an older backend with no `origin`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
Forward the authenticated user's `positions` into the client predicate scope (`current_user.positions`) in the console shell and the record form page. Position-gated select options (`'admin' in current_user.positions`, ADR-0058 / objectui#2284) now hide client-side like they do everywhere else, instead of failing open as visible and only being rejected by the server on submit — `positions` is the actor shape the framework rule-validator actually binds and enforces. Docs, the schema-catalog role-gated example, the skills guide, and inline examples switch the role-gating spelling from `current_user.roles` (never bound server-side, so never enforced) to `current_user.positions`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@object-ui/app-shell': minor
3+
---
4+
5+
Studio object designer: the field inspector's conditional rules (`visibleWhen` / `readonlyWhen` / `requiredWhen`) are now edited with a proper CEL editor — live syntax/semantic validation and autocomplete (object fields after `record.` / `previous.`, the runtime-bound roots `record`/`previous`/`parent`, and the CEL stdlib), backed by the same `@objectstack/formula` validators the server uses. Bare field references are flagged with the exact `record.<field>` fix, the deprecated `conditionalRequired` alias migrates to `requiredWhen` on first edit, and draft validation reports an invalid predicate on any field under its `fields.<field>.<rule>` path before save. (#1582)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@object-ui/app-shell": minor
3+
---
4+
5+
Studio: CEL lint + field autocomplete for condition predicates (#1582).
6+
7+
`ConditionBuilder`'s raw-expression escape hatch — a bare `<textarea>` — is
8+
replaced by `CelPredicateField`, so every surface that authors a condition
9+
through it gains inline syntax/semantic validation and field-name autocomplete
10+
on the canonical `@objectstack/formula` engine:
11+
12+
- field-level `visibleWhen` / `readonlyWhen` / `requiredWhen` (SchemaForm's
13+
`condition` widget auto-maps `/When$/` properties),
14+
- action `visible` / `disabled` (ActionDefaultInspector),
15+
- every other `condition`-widget property (`visibleOn`, `predicate`, …).
16+
17+
The no-code [subject][op][value] builder path is unchanged; only the "Expression"
18+
mode is upgraded. An invalid predicate now surfaces a readable inline error
19+
instead of failing silently at runtime. English + Chinese labels.
20+
21+
This completes the objectui side of #1582 — the CEL assists it asked for now
22+
cover the field `*When` inputs (and, since the previous change, view
23+
`conditionalFormatting` conditions).

.changeset/config.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"@object-ui/create-plugin",
1717
"@object-ui/runner",
1818
"@object-ui/auth",
19-
"@object-ui/tenant",
2019
"@object-ui/permissions",
2120
"@object-ui/mobile",
2221
"@object-ui/plugin-ai",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/app-shell": minor
3+
---
4+
5+
Studio dashboard widget inspector: visual `filterBindings` editor (#2578
6+
item 4, framework#2501). When the dashboard declares filters (`dateRange` /
7+
`globalFilters`), the widget inspector shows a "Dashboard filter bindings"
8+
section with one row per filter: an **Apply** toggle (unticked writes
9+
`filterBindings[name] = false`, opting the widget out) and a field picker
10+
that re-targets the filter to one of THIS widget's fields (empty = default:
11+
the filter's own field). Previously bindings were only configurable through
12+
raw JSON metadata. Filter rows come from the same `resolveDashboardFilterDefs`
13+
normalization the runtime broadcasts from, so the editor offers exactly the
14+
filters the renderer will apply.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@object-ui/core": patch
3+
"@object-ui/plugin-dashboard": patch
4+
"@object-ui/types": patch
5+
---
6+
7+
Fix: a dashboard filter declaring its static `options` in the
8+
`@objectstack/spec` object form (`options: [{ value, label }]` — the shape
9+
the spec validates and what framework-authored dashboards ship) crashed the
10+
whole dashboard with "Objects are not valid as a React child". Caught driving
11+
the showcase Revenue Pulse dashboard in a real browser.
12+
13+
`resolveDashboardFilterDefs` now normalizes both the spec object form and the
14+
bare-string shorthand (`options: ['EMEA']`) to `{ value, label }` pairs —
15+
`DashboardFilterDef.options` is typed accordingly — and the filter bar's
16+
select renders labels (the trigger now shows the selected option's label, not
17+
its raw value). `@object-ui/types` aligns the `GlobalFilterSchema.options`
18+
shape with the spec union.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@object-ui/i18n": patch
3+
"@object-ui/types": patch
4+
---
5+
6+
Dashboard-level filters follow-ups (#2578, framework#2501):
7+
8+
- **i18n**: the `DashboardFilterBar` strings now ship as real locale entries —
9+
`dashboard.filters.*` (bar label, "All time", "Custom…", "All", "Reset",
10+
and the 13 date-range preset labels) added to `en` and `zh`. Previously the
11+
bar always rendered the `useSafeTranslate` English fallbacks.
12+
- **types**: `GlobalFilterSchema.name` and `DashboardWidgetSchema.filterBindings`
13+
landed in `@objectstack/spec` (framework#2501), so the local type
14+
annotations flip from "Pending alignment" to "Aligned" — no shape changes.
15+
16+
Also adds five schema-catalog examples (`plugin-dashboard/filtered-dashboard-*`:
17+
dynamic `optionsFrom` options, text/number/lookup filter types, dataset +
18+
inline widget mix, `targetWidgets` allow-list, date presets + custom range)
19+
and a new "Dashboard-Level Filters" guide page covering the full tutorial,
20+
`page.*` expression usage, and known limitations with workarounds.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@object-ui/react": minor
3+
"@object-ui/core": minor
4+
"@object-ui/plugin-dashboard": minor
5+
---
6+
7+
Dashboard-level filters — the three #2578 item-5 enhancements (framework#2501):
8+
9+
- **react**: nested `PageVariablesProvider`s now MERGE instead of shadowing
10+
wholesale. A filtered dashboard embedded in a Page with its own `variables`
11+
keeps the outer page variables readable inside widget subtrees (`page.*`);
12+
an inner definition shadows only the SAME name; writes route to the scope
13+
that defines the variable (writing an outer-defined name from inside the
14+
nested subtree updates the outer provider); `resetVariables` stays local.
15+
Names defined nowhere still write locally, exactly as before.
16+
- **core**: `buildWidgetScopedFilter` accepts an optional `knownFields` set —
17+
a DEFAULT binding whose target field is not on the widget's object is
18+
skipped with a console warning instead of emitting a query the backend
19+
empty-matches. Explicit `filterBindings` strings are always honoured (a
20+
typo surfaces as a visibly empty widget, never a silently dropped filter).
21+
Omitting `knownFields` preserves the previous unchecked behaviour.
22+
- **plugin-dashboard**: `DashboardRenderer` feeds `knownFields` from
23+
`dataSource.getObjectSchema` for inline `object` widgets (best-effort —
24+
unchecked while metadata loads or when the source can't describe objects).
25+
`optionsFrom` dynamic filter options now resolve DISTINCT values
26+
server-side via a dataset GROUP BY (`queryDataset` with an inline draft)
27+
when the data source supports it, falling back to the previous client-side
28+
top-200 dedupe otherwise.

0 commit comments

Comments
 (0)