Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f94905d
remove(react): #2545 drop unused FormRenderer/FieldFactory duplicate …
os-zhuang Jul 16, 2026
3eff499
test(e2e-live): B3 cascading-options live e2e — client re-filter + se…
os-zhuang Jul 16, 2026
0908391
feat(flow-designer): connector picker lists dispatchable connectors +…
os-zhuang Jul 16, 2026
ba5d541
fix(detail): gate related lists on the current user's child-object re…
baozhoutao Jul 16, 2026
d5b1bc0
remove(tenant): drop zero-consumer @object-ui/tenant package + types/…
os-zhuang Jul 16, 2026
eeef906
feat(app-shell): CEL lint + field autocomplete for condition predicat…
os-zhuang Jul 16, 2026
092bd85
fix(app-shell): bind current_user.positions into the client predicate…
os-zhuang Jul 16, 2026
6c0135c
feat(page-header): metadata-driven multi-button record header (#2361)…
os-zhuang Jul 16, 2026
887062c
feat(dashboard): dashboard-level filters driving multiple charts (fra…
os-zhuang Jul 16, 2026
2efa9fd
fix(detail+fields+app-shell): ADR-0085 #2548 follow-ups — strip title…
os-zhuang Jul 16, 2026
e628d1f
feat(dashboard-filters): #2578 follow-ups — catalog examples, guide t…
os-zhuang Jul 16, 2026
1e87649
fix(app-shell): guard Studio Access pillar against silently discardin…
os-zhuang Jul 16, 2026
0484d03
fix(detail): highlight strip lookup editor honors ObjectStack `refere…
os-zhuang Jul 16, 2026
c1df2e1
feat(app-shell): visual filterBindings editor in the dashboard widget…
os-zhuang Jul 16, 2026
579b24d
feat(fields+form+detail): file/image upload cells in inline line-item…
os-zhuang Jul 16, 2026
5523fc4
feat(dashboard-filters): #2578 item-5 enhancements — nested variable …
os-zhuang Jul 16, 2026
fb35e48
fix(data-objectstack): emit MutationEvents from batchTransaction and …
os-zhuang Jul 16, 2026
2712fc1
fix(fields+detail): resolve pre-existing rules-of-hooks violations in…
os-zhuang Jul 16, 2026
f0f10f5
feat(kanban): default lane field honours the ADR-0085 stageField role…
os-zhuang Jul 16, 2026
9530323
feat(studio): CEL editor with validate + autocomplete for field condi…
os-zhuang Jul 16, 2026
c5461ea
fix(studio): stop force-opening the new-object dialog on empty packag…
os-zhuang Jul 16, 2026
3e8bf07
fix(fields): PeoplePicker cursor resets only on real result changes (…
os-zhuang Jul 16, 2026
2ded18c
fix(dashboard-filters): spec-form filter options crashed the dashboar…
os-zhuang Jul 16, 2026
89b0ee6
fix(core+data-objectstack+app-shell): canonicalize reference/referenc…
os-zhuang Jul 16, 2026
d50977c
feat(flow-designer): pick the target node per branch in the Decision …
os-zhuang Jul 16, 2026
bb6d1b5
fix(app-shell,core): keep error-envelope objects out of toast.error —…
baozhoutao Jul 16, 2026
4550ff0
fix(components): pin sticky leading cells at measured header widths (…
baozhoutao Jul 16, 2026
68dbea4
fix(fields): localize relative-date humanize via Intl.RelativeTimeFor…
baozhoutao Jul 16, 2026
57a61b0
fix(app-shell): lock the Access pillar permission matrix in read-only…
os-zhuang Jul 16, 2026
4b0aee6
fix(plugin-list): spec bare-string sort form crashed ListView (#2578 …
os-zhuang Jul 16, 2026
4ef5b4e
Merge origin/main into claude/github-issue-2358-3b099c — resolve #236…
claude Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .changeset/adr-0085-2548-detail-ux-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
'@object-ui/plugin-detail': patch
'@object-ui/fields': patch
'@object-ui/i18n': patch
'@object-ui/app-shell': patch
---

Detail-page UX follow-ups from the ADR-0085 PR4 real-backend browser pass (framework#2548):

- **Highlight strip no longer repeats the record title.** A declared
`highlightFields` list containing the title field rendered it as the first
chip — truncated — directly under the identical page H1. `deriveHighlightFields`
now resolves the title (`primaryField` / `nameField` / deprecated
`displayNameField`, else the conventional display-field names) via the new
exported `resolveTitleField` and filters it from declared lists before the
4-chip cap, matching what the heuristic branch always did. app-shell's
`RecordDetailView` synthParts (which pre-computes the list and bypasses the
derivation) applies the same filter.
- **Per-field currency reaches the renderers.** The spec channel
(`currencyConfig.defaultCurrency`) was dropped by the highlight-strip and
detail-section field enrichment, so a spec-authored currency field could
never show its symbol ("25,000,000" instead of "$25,000,000");
`resolveFieldCurrency` reads it second after the designer-only bare
`currency` key.
- **app-shell approvals fetches send the Bearer token.** The header badge
poll, home-inbox count, and record-page approvals panel were cookie-only
(new shared `bearerAuthHeaders()` util) — same split-origin failure mode as
the console `approvalsApi` fix below.
- **`fieldGroups[].icon` / `description` reach detail pages.** The shared
derivation (ADR-0085 §5) already passed them through; the detail synth
dropped them. Sections now carry both, and `DetailSection` renders a real
Lucide icon for identifier-shaped names (emoji/text values keep the
historical text rendering).
- **Record meta footer stops dangling without an actor.** Seeded/system rows
with `created_by: null` rendered "Created by · 10m ago"; the footer now
falls back to actor-less labels ("Created / Updated"), with new i18n keys in
all six locales (and the zh `createdBy`/`updatedBy` mistranslation fixed:
创建人/更新人, not 创建于/更新于).
- **Select badges ellipsize instead of clipping mid-glyph.** In bounded
containers (highlight-strip columns, grid cells) an overlong option label
used to be cut at the container edge ("Technolog…"); badges now shrink with
an inner truncate and expose the full label as a hover title. The highlight
strip's hover title also prefers the option label over the raw stored value.

Console app (unversioned): `approvalsApi` now sends the stored Bearer token
like every other console call — cookie-only auth silently lost the approvals
surface on split-origin deployments where the SameSite cookie doesn't flow.
15 changes: 15 additions & 0 deletions .changeset/adr-0096-connector-origin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@object-ui/app-shell": minor
---

feat(flow-designer): connector picker lists dispatchable connectors + marks declarative instances (ADR-0096)

The `connector_action` node's connector picker read `client.list('connector')` —
the declared `connectors:` metadata, which includes inert catalog descriptors and
**misses** plugin-registered connectors. It now reads the runtime registry
(`GET /api/v1/automation/connectors`), i.e. exactly the connectors a
`connector_action` can dispatch: plugin connectors and materialized declarative
instances (framework ADR-0096). Declarative instances are annotated `· declarative`
(from the descriptor's new `origin` field) so authors can tell a materialized
metadata connector apart from a plugin one. Degrades to empty on fetch failure;
the field stays free-text editable. Tolerates an older backend with no `origin`.
5 changes: 5 additions & 0 deletions .changeset/b3-positions-predicate-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@object-ui/app-shell': patch
---

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`.
5 changes: 5 additions & 0 deletions .changeset/cel-editor-field-conditional-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@object-ui/app-shell': minor
---

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)
23 changes: 23 additions & 0 deletions .changeset/condition-builder-cel-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@object-ui/app-shell": minor
---

Studio: CEL lint + field autocomplete for condition predicates (#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. English + Chinese labels.

This completes the objectui side of #1582 — the CEL assists it asked for now
cover the field `*When` inputs (and, since the previous change, view
`conditionalFormatting` conditions).
1 change: 0 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"@object-ui/create-plugin",
"@object-ui/runner",
"@object-ui/auth",
"@object-ui/tenant",
"@object-ui/permissions",
"@object-ui/mobile",
"@object-ui/plugin-ai",
Expand Down
14 changes: 14 additions & 0 deletions .changeset/dashboard-filter-bindings-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@object-ui/app-shell": minor
---

Studio dashboard widget inspector: visual `filterBindings` editor (#2578
item 4, framework#2501). When the dashboard declares filters (`dateRange` /
`globalFilters`), the widget inspector shows a "Dashboard filter bindings"
section with one row per filter: an **Apply** toggle (unticked writes
`filterBindings[name] = false`, opting the widget out) and a field picker
that re-targets the filter to one of THIS widget's fields (empty = default:
the filter's own field). Previously bindings were only configurable through
raw JSON metadata. Filter rows come from the same `resolveDashboardFilterDefs`
normalization the runtime broadcasts from, so the editor offers exactly the
filters the renderer will apply.
18 changes: 18 additions & 0 deletions .changeset/dashboard-filter-spec-options-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@object-ui/core": patch
"@object-ui/plugin-dashboard": patch
"@object-ui/types": patch
---

Fix: a dashboard filter declaring its static `options` in the
`@objectstack/spec` object form (`options: [{ value, label }]` — the shape
the spec validates and what framework-authored dashboards ship) crashed the
whole dashboard with "Objects are not valid as a React child". Caught driving
the showcase Revenue Pulse dashboard in a real browser.

`resolveDashboardFilterDefs` now normalizes both the spec object form and the
bare-string shorthand (`options: ['EMEA']`) to `{ value, label }` pairs —
`DashboardFilterDef.options` is typed accordingly — and the filter bar's
select renders labels (the trigger now shows the selected option's label, not
its raw value). `@object-ui/types` aligns the `GlobalFilterSchema.options`
shape with the spec union.
20 changes: 20 additions & 0 deletions .changeset/dashboard-filters-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@object-ui/i18n": patch
"@object-ui/types": patch
---

Dashboard-level filters follow-ups (#2578, framework#2501):

- **i18n**: the `DashboardFilterBar` strings now ship as real locale entries —
`dashboard.filters.*` (bar label, "All time", "Custom…", "All", "Reset",
and the 13 date-range preset labels) added to `en` and `zh`. Previously the
bar always rendered the `useSafeTranslate` English fallbacks.
- **types**: `GlobalFilterSchema.name` and `DashboardWidgetSchema.filterBindings`
landed in `@objectstack/spec` (framework#2501), so the local type
annotations flip from "Pending alignment" to "Aligned" — no shape changes.

Also adds five schema-catalog examples (`plugin-dashboard/filtered-dashboard-*`:
dynamic `optionsFrom` options, text/number/lookup filter types, dataset +
inline widget mix, `targetWidgets` allow-list, date presets + custom range)
and a new "Dashboard-Level Filters" guide page covering the full tutorial,
`page.*` expression usage, and known limitations with workarounds.
28 changes: 28 additions & 0 deletions .changeset/dashboard-filters-item5-enhancements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@object-ui/react": minor
"@object-ui/core": minor
"@object-ui/plugin-dashboard": minor
---

Dashboard-level filters — the three #2578 item-5 enhancements (framework#2501):

- **react**: nested `PageVariablesProvider`s now MERGE instead of shadowing
wholesale. A filtered dashboard embedded in a Page with its own `variables`
keeps the outer page variables readable inside widget subtrees (`page.*`);
an inner definition shadows only the SAME name; writes route to the scope
that defines the variable (writing an outer-defined name from inside the
nested subtree updates the outer provider); `resetVariables` stays local.
Names defined nowhere still write locally, exactly as before.
- **core**: `buildWidgetScopedFilter` accepts an optional `knownFields` set —
a DEFAULT binding whose target field is not on the widget's object is
skipped with a console warning instead of emitting a query the backend
empty-matches. Explicit `filterBindings` strings are always honoured (a
typo surfaces as a visibly empty widget, never a silently dropped filter).
Omitting `knownFields` preserves the previous unchecked behaviour.
- **plugin-dashboard**: `DashboardRenderer` feeds `knownFields` from
`dataSource.getObjectSchema` for inline `object` widgets (best-effort —
unchecked while metadata loads or when the source can't describe objects).
`optionsFrom` dynamic filter options now resolve DISTINCT values
server-side via a dataset GROUP BY (`queryDataset` with an inline draft)
when the data source supports it, falling back to the previous client-side
top-200 dedupe otherwise.
39 changes: 39 additions & 0 deletions .changeset/dashboard-level-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
"@object-ui/types": minor
"@object-ui/core": minor
"@object-ui/plugin-dashboard": minor
"@object-ui/plugin-report": patch
---

feat(dashboard): dashboard-level filters (date / region) driving multiple charts (framework#2501)

A dashboard's `dateRange` + `globalFilters` declarations are now wired end to
end: the filter values live as dashboard-level variables (the page variables
primitive, so they're also readable as `page.<name>` in widget expressions),
a filter bar renders above the widgets, and at render time the dashboard
broadcasts the active values into every bound widget's inline query —
`AND`-merged with the widget's own `filter`. Charts stay inline and
self-contained; each widget maps a filter to **its own** field.

- **`@object-ui/types`** — `globalFilters[].name` (stable filter/variable key,
defaults to `field`) and `DashboardWidgetSchema.filterBindings`
(`Record<string, string | false>`: per-widget field override / `false`
opt-out). Zod mirrors included. **Pending paired `@objectstack/spec`
alignment (framework#2501)** — same precedent as `dataset` /
`categoryGranularity`.
- **`@object-ui/core`** — new pure `dashboard-filters` module
(`resolveDashboardFilterDefs`, `dashboardFilterVariableDefs`,
`buildFilterCondition`, `buildWidgetScopedFilter`); `mergeFilters` lifted
from plugin-report (re-exported there unchanged). Date presets emit
date-macro tokens (`{30_days_ago}` …) so widgets resolve them at query time
like hand-authored filters.
- **`@object-ui/plugin-dashboard`** — `DashboardFilterBar` (date presets +
custom range calendar, select with static `options` or `optionsFrom`,
text/number inputs, reset); `DashboardRenderer` mounts a
`PageVariablesProvider` when filters are declared and merges the
widget-scoped condition into inline widgets' `filter` and dataset widgets'
`runtimeFilter`. Dashboards without filters render exactly as before.

Binding precedence: explicit `filterBindings` string/`false` → legacy
`targetWidgets` allow-list → the filter's own `field` (dateRange defaults to
`created_at`). Static-data widgets are not filtered.
24 changes: 24 additions & 0 deletions .changeset/field-file-inline-grid-2360.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@object-ui/fields": minor
"@object-ui/plugin-form": minor
"@object-ui/plugin-detail": minor
---

feat(fields+form+detail): file/image uploads in inline line-item grids (#2360)

`Field.file` in a master-detail inline grid previously degraded to a plain text
input (no `input[type=file]` on the page → no way to upload from the grid), and
auto-derived subform / related-list columns silently dropped file fields.

- **fields**: new `FileCell` — a compact upload control for grid cells (upload
button + removable chips, image thumbnails), sharing the `UploadProvider`
pipeline with the full-size `FileField` via an extracted `useFileUploads`
hook. `GridField` supports `type: 'file'` columns (with `accept` /
`multiple`), renders file names in list/readonly modes, and no longer falls
back to a text `<Input>` for file columns.
- **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 through the existing FileCellRenderer /
ImageCellRenderer (file-name chip / thumbnail).
26 changes: 26 additions & 0 deletions .changeset/flow-decision-branch-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@object-ui/app-shell": minor
---

feat(flow-designer): pick the target node per branch in the Decision Branches editor (#1942)

The decision node's Branches editor gains a **Target** column — a node picker
scoped to the flow's own nodes — so a business user can author the whole
decision (conditions *and* destinations) in one table, mirroring Salesforce
Flow Decision Outcomes. Completes #1930 (the per-edge Branch picker) from the
node side.

- The column is **virtual**: its value is derived from the decision's outgoing
edges (the routing source of truth) and never persisted on
`config.conditions`, so it round-trips with the `FlowEdgeInspector` Branch
picker and canvas rewiring for free.
- Picking a target creates the branch's out-edge if missing, or updates /
retargets the existing one in place, carrying the branch's condition, label,
and default flag. Clearing a target detaches (removes) that branch's edge —
never the node it pointed at. Custom per-edge guards, fault/back edges, and
surplus canvas wiring are never touched.
- A branch list committed with no targets anywhere (e.g. an engine-published
configSchema form without the column) keeps the legacy #1927 by-order edge
mirror, byte-for-byte.
- New pure module `flow-decision-edges.ts` with unit tests for the
branch→edge reconciliation.
14 changes: 14 additions & 0 deletions .changeset/listview-string-sort-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@object-ui/plugin-list": patch
---

Fix: a view declaring its `sort` in the `@objectstack/spec` bare-string
top-level form (`sort: "name desc"` — `ListViewSchema.sort` is
`string | Array<{field, order}>`) crashed ListView with
"schema.sort.map is not a function". Found by the spec/renderer
shape-mismatch audit that followed the dashboard filter-options crash.
Sort parsing is now a single normalized `parseSortConfig` (exported) that
accepts the bare string, legacy `"field desc"` array entries, and
`{ field, order }` objects, and returns `[]` for malformed entries instead
of throwing. The `@object-ui/types` declaration already carried the union —
only the implementation missed the string branch.
13 changes: 13 additions & 0 deletions .changeset/people-picker-cursor-reset-flake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@object-ui/fields": patch
---

fix(fields): PeoplePicker keyboard cursor no longer resets on identity-only
result re-emissions

The cursor-reset effect keyed on the records array identity, so a background
refetch returning the same records (StrictMode double-effect, refetch-on-focus)
yanked the active row back to none mid-navigation — surfacing as a flaky
ArrowDown→Enter CI test and a real (if rare) keyboard UX glitch. The reset is
now keyed on the record-id signature, so the cursor only resets when the
results actually change.
28 changes: 28 additions & 0 deletions .changeset/record-header-multi-primary-buttons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@object-ui/components": minor
"@object-ui/app-shell": minor
---

feat(page-header): metadata-driven multi-button record header (#2361)

The record detail page header no longer hardcodes a single inline primary
button (`INLINE_MAX = 1`). It now renders up to `maxVisible` actions
side-by-side (default 3 desktop / 1 mobile, overridable via
`maxVisible` / `mobileMaxVisible` on the `page:header` schema) — the same
contract as `action:bar` — so multi-action objects (e.g. Lead: Convert /
Assign / Return) can surface several primary buttons at once.

Which actions claim the inline slots is declared in metadata, mirroring the
`action:bar` #2339 rules:

- `order` ascending (unset = 0; lower = more prominent), stable sort;
- `variant: 'primary'` as a tie-break within equal order (also mapped to the
Shadcn `default` Button variant instead of leaking through);
- `component: 'action:menu'` pins an action inside the `⋯` overflow menu
regardless of the action count.

The synthesized system actions declare their placement accordingly:
`sys_edit` gets `order: 100` (behind every authored business action, but
still inline when slots remain), while `sys_share` / `sys_delete` are pinned
into the `⋯` menu via `component: 'action:menu'` — Delete never surfaces as
an inline red button just because an object has few actions.
27 changes: 27 additions & 0 deletions .changeset/remove-dead-tenant-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@object-ui/types": minor
---

remove(tenant): drop the zero-consumer `@object-ui/tenant` package and the `types/tenant.ts` mirror (#2564)

`@object-ui/tenant` (`TenantProvider` / `TenantGuard` / `TenantScopedQuery` /
`createTenantResolver` / `useTenant` / `useTenantBranding`) was an
exported-but-dead aspirational surface: no workspace package depended on it
and nothing imported it. Its `TenantConfig.isolation` strategy enum
(`'database' | 'schema' | 'row' | 'hybrid'`) was the UI mirror of the spec's
`tenancy.strategy`, which framework#2763/framework#2962 removed under the same
enforce-or-remove doctrine — the platform has exactly two tenancy modes, and
neither is configured client-side.

`@object-ui/types` no longer exports the tenant type family
(`TenantConfig`, `TenantIsolationStrategy`, `TenantStatus`, `TenantPlan`,
`TenantBranding`, `TenantLimits`, `TenantContext`,
`TenantResolutionStrategy`, `TenantProviderConfig`,
`TenantScopedQueryConfig`).

Migration: real tenant scoping is server-enforced — `createAuthenticatedFetch`
(`@object-ui/auth`) already injects the active organization as `X-Tenant-ID`
on every API call, and the backend applies row-level isolation
(`tenancy.enabled` + `tenantField` in `@objectstack/spec`). Per-tenant
branding is a `ThemeSchema` concern. The skills guides and docs that
advertised the dead package have been rewritten to say exactly that.
Loading