diff --git a/.changeset/action-disabled-all-renderers.md b/.changeset/action-disabled-all-renderers.md deleted file mode 100644 index a8363b5424..0000000000 --- a/.changeset/action-disabled-all-renderers.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@object-ui/components": patch -"@object-ui/app-shell": patch -"@object-ui/plugin-detail": patch ---- - -fix(action): honor the spec `disabled` predicate on every action-rendering surface (#1885 follow-through) - -The spec Action field is `disabled` (boolean | CEL — disabled when TRUE); the -schema has no `enabled` key. #1885 wired it in `action:button` only. Browser -dogfooding against the showcase found FIVE more surfaces where a spec-authored -`disabled` silently did nothing: - -- **components** — the `action:group` leaves (inline + dropdown), `action:icon` - and `action:menu` still read the legacy non-spec `enabled`. They now consume - `disabled` as the primary control (evaluated in the same scope as `visible`), - with `enabled` kept as a deprecated fallback. -- **app-shell** — `DeclaredActionsBar` (server-declared action bar) read - neither; it gains `disabled` (no legacy fallback: declared actions are - spec-shaped and never carried `enabled`). -- **plugin-detail** — `record:quick_actions` HAD a `disabled` implementation, - but its `typeof === 'string'` split dropped the `{dialect:'cel', source}` - envelope the server compiles authored CEL into (#2661 routes envelopes to the - canonical formula engine), so the predicate never fired on real metadata. It - now feeds `toPredicateInput`'s result to `useCondition` whole, like every - other surface. - -Pinned by new `DropdownActionItem` tests (disabled-when-TRUE, false-stays- -clickable, disabled-wins-over-enabled, boolean literal) and browser-verified -end-to-end against the showcase `showcase_archive_task` specimen: greyed on an -in-progress task, clickable on a done one (with `visible` hiding Mark Done on -the same screen — the hide-vs-grey contrast). diff --git a/.changeset/action-param-inline-lookup.md b/.changeset/action-param-inline-lookup.md deleted file mode 100644 index dc859a130f..0000000000 --- a/.changeset/action-param-inline-lookup.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/i18n": patch ---- - -fix(app-shell): give inline `lookup` action params a real record picker (#3405) - -An action parameter declared inline as `{ name: 'inspector', type: 'lookup', -reference: 'sys_user' }` always rendered as a plain text input asking the user -to paste a record id (UUID) — a supervisor assigning an inspector had to go -find that person's UUID by hand, while the same reference field picks records -by name in the create/edit dialog. - -`paramToField()` degrades a picker param to text when it has no `referenceTo` -target, and `referenceTo` was only ever populated on the field-backed branch of -`resolveActionParams()`. The inline branch dropped the authored `reference` -key entirely (as did the spec schema, which stripped it as unknown), so an -inline picker could never reach `` no matter how it was authored. - -- `resolveActionParam()` now maps an inline `reference` onto `referenceTo` — on - the inline branch, on the missing-field fallback branch, and as an override - on the field-backed branch (matching how every other inline value overrides - the resolved field). -- The text degradation now warns in dev naming the offending param, since with - `@objectstack/spec` rejecting a targetless inline picker at parse time it - means the metadata is broken, not merely partial. -- The fallback's placeholder and help text no longer claim "a picker is coming - soon" — the picker has shipped, and the message now says the parameter has no - reference object configured. Updated across all 10 locales. diff --git a/.changeset/activity-popover-field-mapping.md b/.changeset/activity-popover-field-mapping.md deleted file mode 100644 index 3f0f1c8082..0000000000 --- a/.changeset/activity-popover-field-mapping.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(app-shell): map raw `sys_activity` rows before rendering the inbox Activity tab - -The top-bar inbox bell's Activity tab (`InboxPopover`) rendered blank rows — -only the relative time showed (`47m ·`), with the actor, summary, and object -name all missing. `AppHeader.fetchPresenceAndActivities` cast the raw -`sys_activity` rows straight to `ActivityItem` without renaming their fields, -so the popover read `a.user` / `a.description` / `a.objectName` while the rows -only carry plugin-audit's `actor_name` / `summary` / `object_name`. - -The rows are now mapped onto `ActivityItem` (with `type` normalization, a -`timestamp` fallback, and an empty-`summary` filter), mirroring the mapping in -`useHomeInbox` so the bell and the Home dashboard stay in sync. diff --git a/.changeset/adr-0105-group-posture-console.md b/.changeset/adr-0105-group-posture-console.md deleted file mode 100644 index 77161738f2..0000000000 --- a/.changeset/adr-0105-group-posture-console.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@object-ui/auth": minor -"@object-ui/app-shell": minor ---- - -feat(console): group tenancy posture affordances — org switcher as write -context + org attribution in read views (framework ADR-0105 Phase 1) - -Under the new `group` tenancy posture the server widens reads to every -organization the member belongs to (`organization_id IN accessible_org_ids`) -while writes land in the ACTIVE organization — so the console's existing -"which org am I in = which org's data I see" presentation becomes wrong the -moment a deployment switches postures. The ADR requires these affordances to -land WITH Phase 1, not after. - -- `@object-ui/auth`: `AuthPublicConfig.features.tenancyPosture` - (`'single' | 'group' | 'isolated'`, exported as `TenancyPosture`) mirrors - the server's public auth config key. It gates nothing — `multiOrgEnabled` - stays the capability flag; this only tells the console how to render org - context. -- `useTenancyPosture()` (app-shell): reads the posture from the cached auth - config fetch; `undefined` (older server, unrecognized value, fetch failure) - keeps every group affordance off, so non-group deployments render - pixel-identical to today. -- `WorkspaceSwitcher`: under `group` the dropdown labels the active org - "Working organization" and explains the split — new records are created - here, views show data from all your organizations. -- `RecordFormPage` (create mode): org-walled objects show a "Creates in - " badge naming the engine's write target (ADR-0105 D5 stamps - `organization_id` from the active org). -- Default list columns (`ObjectView`, `InterfaceListPage`, `ObjectDataPage`): - under `group`, org-walled objects get a TRAILING `organization_id` - attribution column so cross-org rows are attributable at a glance. - Render-time only — never persisted into saved view/page metadata, and - business fields still lead. diff --git a/.changeset/ai-model-status-summary-i18n.md b/.changeset/ai-model-status-summary-i18n.md deleted file mode 100644 index ada75a889a..0000000000 --- a/.changeset/ai-model-status-summary-i18n.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/app-shell": patch ---- - -fix(i18n): compose the AI-model diagnostics summary client-side instead of rendering the server's English string (objectui#2886) - -`CloudAiModelStatus` rendered `report.summary` verbatim — the most prominent -line on the panel, in English for every locale. - -Reading `objectstack-ai/cloud` settled how to fix it. The server **cannot** -localize that string as currently built: - -- `service-ai/src/effective-model.ts:117` assembles it as a hard-coded English - template literal, with no locale parameter; -- `service-ai/src/routes/ai-routes.ts:395` declares `handler: async () => …` — - it takes **no request argument**, so it cannot read `Accept-Language` even - though `createAuthenticatedFetch` has been sending it since objectui#1319. - -But no server change is needed, because every ingredient of the sentence is -already in the structured payload: `conversational.model`, -`conversational.source`, `structured.model`, `structured.pinned`, and -`routing.{free,paid}`. The issue proposed "return structured data instead of a -sentence" as the better fix — the server was already doing that; the client -just wasn't using it. - -The panel now composes the line from those fields. `sourceLabel()` already -produced exactly the two clauses the server hand-rolls — "pinned by X" / -"code default (no env override)", and "same as build/ask" for an unpinned -structured model — so no new source vocabulary was required. - -**A dropped diagnostic, not just untranslated text.** The client's -`EffectiveModelReport` never declared `routing`, which the server has always -sent conditionally. Its only appearance anywhere was inside the English summary, -so non-English admins could not see the plan→model routing policy **at all**. -It is now declared and surfaced. - -Also fixed: `attributeSource` emits the bare token `'unknown'` when the adapter -cannot report a model, and `sourceLabel` fell through to rendering it raw. - -Four keys added to all ten packs (`summary`, `summaryRouting`, `modelUnknown`, -`sourceUnknown`), so the full-parity guard from objectui#2909 stays green. - -The panel had **no test coverage at all**; it now has five, mutation-tested by -restoring `

{report.summary}

` — which fails four of them. diff --git a/.changeset/approval-approver-group-chips.md b/.changeset/approval-approver-group-chips.md deleted file mode 100644 index c03c3c2d1e..0000000000 --- a/.changeset/approval-approver-group-chips.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/console": patch ---- - -feat(approvals): label pending-approver chips with their group (objectui#2807) - -Follow-up to #2762 P1-2. The dedupe pass collapsed repeated "waiting on" -chips to one with a `×N` count, but couldn't say *which* group (finance / -legal / …) each pending approver represented in a 会签 (per_group) request — -the data wasn't there. With the framework now emitting -`pending_approver_groups` (`@objectstack/plugin-approvals`), the drawer: - -- keys the chip collapse by **(name, group)** — the same person filling two - different groups stays two labeled chips (`Dev Admin · finance`, - `Dev Admin · legal`), while one group filled twice collapses to a single - chip with a count; -- renders the group as a muted `· ` sub-tag on the chip. - -Degrades cleanly: with no group data (non-`per_group`, or an older backend) -the key is the name alone, keeping the plain dedupe + `×N` behavior. diff --git a/.changeset/approval-approver-record-lookups.md b/.changeset/approval-approver-record-lookups.md deleted file mode 100644 index 800ff84568..0000000000 --- a/.changeset/approval-approver-record-lookups.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/fields': patch ---- - -feat(app-shell): approval approver values become record lookups (framework #3508) - -- The flow designer's approver `Value` cell now sources directory kinds from DATA - records instead of the metadata registry: `user` / `team` / `department` / `position` - render a single-select record lookup (`LookupField` over `sys_user` / `sys_team` / - `sys_business_unit` / `sys_position` via the DataSource adapter), with a manual-entry - escape hatch and a plain free-text fallback when no adapter is available (offline - preview). `position` commits the machine name; the others commit the record id — - matching the approval engine's resolution semantics. -- `org-membership-level` is now a strict select (owner/admin/member); a stored - out-of-enum value renders flagged instead of being blanked. -- `manager` renders as an auto-resolved (disabled) cell; `queue` is no longer offered - for new approver rows and stored queue rows carry a "not supported by the runtime" - warning. -- `@object-ui/fields`: `LookupField` hydrates the selected label through `id_field` - when it is not the primary id (e.g. `id_field: 'name'`), instead of always calling - `findOne` with the primary id. diff --git a/.changeset/approval-attachment-chip.md b/.changeset/approval-attachment-chip.md deleted file mode 100644 index c0b2acf18c..0000000000 --- a/.changeset/approval-attachment-chip.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@object-ui/console": patch -"@object-ui/i18n": patch ---- - -fix(console): make the approval timeline attachment chip show its name and open (#2820) - -A decision attachment in the approval inbox timeline (审批动态) rendered a -nameless "附件" chip that did nothing when clicked. Three separate bugs: - -- **No filename.** The chip resolved its label by fetching `/data/sys_file/{id}` - — a system object a regular approver cannot read — and silently fell back to a - generic label when that was denied. The name now comes from the attachment - descriptor the server returns (framework #3266), so no `sys_file` access is - needed and the real filename shows for every approver. -- **Dead click.** `openAttachment` called `window.open` *after* an `await`, so - it was no longer a user gesture and the browser blocked the popup. It now opens - the tab synchronously up front, then points it at the signed URL once fetched. -- **Wrong origin.** The signed URL from the local storage adapter is - server-relative; `window.open` resolved it against the console origin. It is - now resolved against the API origin. -- Every open failure was swallowed silently. The user now gets a toast on - failure — new `approvalsInbox.attachmentOpenFailed` string across all 10 - locales. diff --git a/.changeset/approval-band-honors-lock-record.md b/.changeset/approval-band-honors-lock-record.md deleted file mode 100644 index 9654610d73..0000000000 --- a/.changeset/approval-band-honors-lock-record.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"@object-ui/react": minor -"@object-ui/plugin-detail": minor -"@object-ui/app-shell": minor -"@object-ui/i18n": minor ---- - -fix(detail): the approval band honors the node's `lockRecord` instead of assuming every approval locks (#2902) - -A record detail page treated "a pending approval request exists" as "this -record is locked". An approval node declares `lockRecord` (default `true`), and -on `lockRecord: false` the server keeps accepting writes for the whole time -that node waits — so the console was asserting a lock the backend did not -enforce. - -The label was the smaller half of it. The same conflated signal fed `canEdit`, -so the record-level inline-edit session was suppressed too: no pencils, -`enter()` a no-op. On a single-approver step — a department head or plant -manager, exactly the case `lockRecord: false` exists for, where the approver is -meant to fill in the missing detail before deciding — the capability was -unreachable from the UI. And a flow chaining nodes with different policies drew -one identical band for "edit freely" and "the server will reject your save with -`RECORD_LOCKED`", so the two states were indistinguishable until Save failed. - -Approval state is now two signals: - -- **`approvalPending`** — an approval is running. Drives the band and the recall - button, both meaningful whether or not the record is editable. -- **`locked`** — that approval also forbids edits, from the pending node's - `lock_record` (framework#3814, read off the same `node_config_json` snapshot - the server's record-lock hook reads). - -The band renders two states: amber lock + "Locked for approval", or sky clock + -"In approval · editable", each with its own tooltip. Recall moved out of the -locked branch — an editable pending approval is just as recallable. Inline -editing stays live in the editable state. - -`InlineEditProvider` takes a new optional `approvalPending` prop, defaulting to -`locked`, so a host that threads only `locked` renders exactly as before. The -record's `approval_status` field remains the fallback for backends with no -approvals API; it carries no node granularity, so it still reads as locked — as -does a pending request from a backend too old to report the policy. - -New `detail.approvalPendingEditable` / `detail.approvalPendingTooltip` keys are -translated in all ten locales. diff --git a/.changeset/approval-center-density-amount.md b/.changeset/approval-center-density-amount.md deleted file mode 100644 index c3a9392a6b..0000000000 --- a/.changeset/approval-center-density-amount.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/console": patch ---- - -fix(approvals): Approval Center density + amount emphasis (#2762 P2) - -- **Column rebalance** — the inbox table gave five auto columns equal width, - leaving 审批事项 (Request) over-wide next to a cramped 状态 (Status). The - Record column (the primary content) now gets the widest share, Request a - moderate one, and Status/Submitted fixed widths so they never crowd. -- **Lead with the amount** — the drawer summary card now surfaces the - decision-critical amount as a filled figure at the top of the card instead - of burying it in the generic field grid (and drops it from that grid so it - shows once). - -Also verified two P2 items need no change: light mode already works — -`ConsoleShell` mounts `ThemeProvider defaultTheme="system"` (follows the OS -`prefers-color-scheme`) with a `ModeToggle`, and the page's own classes carry -full light/dark variants; and the queue already has a bulk approve/reject -toolbar for the select-all/per-row selection. diff --git a/.changeset/approval-center-optimization.md b/.changeset/approval-center-optimization.md deleted file mode 100644 index 4bfe9d9996..0000000000 --- a/.changeset/approval-center-optimization.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@object-ui/components": patch -"@object-ui/app-shell": patch -"@object-ui/i18n": patch ---- - -fix(approvals): Approval Center UX pass — badge nowrap, approve confirm, decision progress bar, localized declared actions (#2762) - -- **Badge no longer stacks CJK text vertically (P0-1)** — `Badge` gains - `whitespace-nowrap` in its base variants (a badge is a single-line pill by - definition), and the inbox 状态 column gets a minimum width, so 待审批 can - never render as 待/审/批. -- **Quick Approve now confirms (P0-2)** — the row's right-edge ✓, the mobile - card button and the `a` keyboard shortcut all route through a confirmation - dialog before executing, mirroring the Reject flow; an irreversible decision - can no longer fire on a stray click. -- **Decision progress is visualized (P1-1)** — the drawer renders a segmented - progress bar (ARIA `progressbar`) for `decision_progress`, per-group chips - get an explicit unsatisfied ○ state next to the satisfied ✓, the eligible - approver count is spelled out, and the drawer pager now reads - "Request N of M" so it can't be misread as approval progress. -- **Declared action labels localize (P0-3)** — `DeclaredActionsBar` resolves - label / confirmText / successMessage through the `_actions..*` - translation convention (metadata literals as fallback), matching - ObjectView/RecordDetailView; with the `@objectstack/plugin-approvals` - bundle, the drawer shows 通过 / 拒绝 / 转签 instead of English in a zh-CN - workspace. New `approvalsInbox` keys shipped in all ten locales. diff --git a/.changeset/approval-center-triage-pass.md b/.changeset/approval-center-triage-pass.md deleted file mode 100644 index cd7d17eee2..0000000000 --- a/.changeset/approval-center-triage-pass.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/i18n": patch ---- - -fix(approvals): Approval Center triage + drawer readability pass (#2762 P1-2/P1-3/P1-4/P1-5/P2) - -- **Decision-relevant data in the queue (P1-3)** — list rows and mobile cards - now surface the request's amount/total inline (detected from the snapshot, - preferring the server-formatted `payload_display` value), so a reviewer can - triage without opening each request. A sort control adds "Oldest first" and - "Amount (high→low)" alongside the default newest-first. -- **Empty applicant column (P1-4)** — flow-/system-initiated requests (no human - submitter) now read "Flow-initiated" with a workflow icon instead of a bare - person icon + "—", in the desktop table, mobile card, and drawer. -- **Approver chips deduped (P1-2)** — a person filling more than one approver - slot rendered as N identical "Waiting on" chips; they collapse to one chip - with a ×N count, the tooltip keeping every underlying id. -- **Action hierarchy (P1-5)** — `DeclaredActionsBar` maps the spec action - `variant` enum onto the Button variants (`primary` → filled default, - `danger` → destructive), so the drawer's Approve stands out and Reject reads - as destructive once `@objectstack/plugin-approvals` declares them. -- **Label polish (P2)** — `owner_id`-style resolved lookup keys render as - "Owner", not the awkward "Owner Id", in the drawer summary. - -New `approvalsInbox` keys (`flowOrigin`, `sortBy`/`sortRecent`/`sortOldest`/ -`sortAmount`) added to all ten locales. diff --git a/.changeset/approval-dynamic-outputs-expression.md b/.changeset/approval-dynamic-outputs-expression.md deleted file mode 100644 index b66f38902f..0000000000 --- a/.changeset/approval-dynamic-outputs-expression.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@object-ui/app-shell": minor -"@object-ui/console": minor ---- - -feat(approvals): dynamic decision-output fields + expression approver editing (framework#3447 P2) - -- The approve/reject dialogs now render one input per author-declared decision-output key (the row's `decision_outputs`, per-request so it can't be a static action param). DeclaredActionsBar synthesizes `outputs.` params; the api handler folds them into the nested `outputs` body the decide route expects. Blank optional outputs are omitted. -- The flow designer's approval-node approver list renders an `expression`-type approver's value as a CEL expression input (mono + syntax check) instead of a dead free-text reference box, with a placeholder teaching the three legal roots (`current.*` / `trigger.*` / `vars.*`). Flow-scope pickers are deliberately not wired in — approval expressions have their own closed root set, and offering flow-scope paths would teach exactly the spelling the runtime rejects. -- Static fallback descriptor gains the `Expression (CEL)` approver type, the expression-only `resolveAs` column, and the node-level `onEmptyApprovers` policy select (the online form derives all of these from the engine's published configSchema). diff --git a/.changeset/approval-typed-picker-quickpath-completion.md b/.changeset/approval-typed-picker-quickpath-completion.md deleted file mode 100644 index 9a4acf1747..0000000000 --- a/.changeset/approval-typed-picker-quickpath-completion.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@object-ui/app-shell": minor -"@object-ui/console": minor ---- - -feat(approvals): typed decision-output pickers, quick-path guard, and approval-expression completion (framework#3447 follow-ups, #2829) - -- Decision dialogs render TYPED decision outputs as record pickers: `decision_output_defs` (`{ key, label?, type, multiple? }`) maps `user` to the sys_user people picker and `department`/`position`/`team` to the matching system-object lookup; `multiple` collects an id array. Bare keys keep the text input. -- Quick decision paths (inline a/r keyboard, hover buttons, mobile card buttons, bulk apply) no longer decide a request whose node declares decision outputs (#2829) — only the drawer dialog collects those fields. Buttons render disabled with an explanation; bulk selection excludes such rows via the existing "N actionable" messaging. -- The approval `expression` approver input now has a scope-aware data picker and inline root validation: three groups — `current.` (live at node entry), `trigger.` (submit snapshot), `vars.*` (flow variables) — built by `useFlowScope` from the same materials as the condition picker but with the approval root set. `nodeOutputRefs` now models approval nodes (`.decision` + declared `decisionOutputs` keys), so the previous stage's outputs are pickable, and `vars.previous` is always listed so a legitimate `vars.*` reference is never flagged as out of scope. diff --git a/.changeset/approvals-inbox-i18n.md b/.changeset/approvals-inbox-i18n.md deleted file mode 100644 index d93a0c4aae..0000000000 --- a/.changeset/approvals-inbox-i18n.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@object-ui/fields": patch -"@object-ui/i18n": patch -"@object-ui/console": patch ---- - -fix(i18n): localize FileField upload widget + approvals snapshot field labels - -- `FileField` (the shared upload widget) hard-coded every visible string - ("Drag & drop files here", "or click to browse", "Take photo", "Uploading…", - size/upload validation messages, …). They now route through - `useObjectTranslation` with new `fields.file.*` keys, translated across all - 10 locale bundles. This is why the approvals Approve/Reject dialog's - attachment dropzone was English in a Chinese console. -- The approvals inbox record-snapshot summary title-cased raw machine keys - instead of the target object's field labels. It now consumes the - server-sent `payload_labels` in `payloadSummary`/`decisionAmountEntry`, - falling back to the prettified key when absent; `approvalsApi`'s row type - gains `payload_labels`. diff --git a/.changeset/approver-value-sources-from-schema.md b/.changeset/approver-value-sources-from-schema.md deleted file mode 100644 index 683a9d61b2..0000000000 --- a/.changeset/approver-value-sources-from-schema.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(flow-designer): read approver value sources off the schema instead of mirroring them (framework#3508 follow-up) - -The approver Value picker decided *where its candidates live* from a local -table, `KIND_TO_RECORD_LOOKUP`, hand-mirrored from the spec's -`APPROVER_VALUE_BINDINGS`. That mirror is what made framework#3508 possible: -`xRef.map` names a picker KIND (`'team'`) and nothing more, so this package had -to pick a data source itself — and picked the metadata REGISTRY -(`GET /api/v1/meta/:type`), which lists no `sys_user` / `sys_team` / -`sys_business_unit` / `sys_position` ROWS. Candidates were always empty and the -control degraded to a raw-id text box. - -The spec now publishes the data contract as `xRef.sources` (one entry per -approver type: `{ source: 'data', object, valueField }`, the closed enum -inline, or a non-picker marker). `json-schema-to-fields` carries it through — -validating each entry, dropping any that could not drive a picker — and -`recordLookupFor()` prefers it over the local table. A new approver type can no -longer leave a stale mirror behind here. - -What did NOT move: presentation. Which field to display, whether to open the -people picker, what subtitle to show under a row stay this package's calls, so -the spec ships the data contract and not the look. The local table remains as -the fallback for a server that predates the annotation, and a `data` source for -a kind with no presentation entry still renders a lookup labelled by its -committed column — better than degrading a resolvable reference to free text. - -Also corrects the approver `type` options comment in `flow-node-config.ts`: that -list is the OFFLINE fallback (`FlowNodeInspector` renders -`serverFields ?? fieldsForNodeType(...)`, so a real backend's published -configSchema wins). Its "indirect bindings lead, `user` last" ordering therefore -never reached the live picker, which followed the spec enum with `user` first — -the opposite of the intent. The ordering now lives in the spec's `ApproverType` -enum, and the comment says which list is authoritative. diff --git a/.changeset/attachment-error-envelope.md b/.changeset/attachment-error-envelope.md deleted file mode 100644 index 54d9e96cd8..0000000000 --- a/.changeset/attachment-error-envelope.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(attachments): read the storage service's new error envelope so gated downloads keep their friendly copy (objectstack#3675) - -`RecordAttachmentsPanel` mapped the server's fail-closed 40x codes -(`AUTH_REQUIRED`, `ATTACHMENT_DOWNLOAD_DENIED`) to human copy by reading -`code` off the top level of the error body. The storage service has moved that -code into the envelope its contract declares — -`{ success: false, error: { code, message } }` — so the top-level read now -returns `undefined`, and every gated download would have degraded from -"You don't have access to download this attachment." to the generic -"Download failed (403)". - -The download handler now reads `body?.error?.code ?? body?.code`, mirroring how -the success branch two lines below already reads `body?.url ?? body?.data?.url`. -Both dialects on purpose: the console ships independently of the server it -talks to, so a current console must keep understanding an older one. A test -covers each shape, and the fix is mutation-checked — dropping the nested read -fails the two new-envelope cases. diff --git a/.changeset/autolist-owner-id-columns.md b/.changeset/autolist-owner-id-columns.md deleted file mode 100644 index a38650ab2c..0000000000 --- a/.changeset/autolist-owner-id-columns.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(list): keep the injected `owner_id` out of the auto-generated list columns - -`ObjectView` renders an object's default "所有记录" tabular view (and prefills the -"Add View" dialog) from the object's field order when it declares no explicit -list view. Both paths carried their own name-based `SYSTEM_FIELDS` exclusion set -that — like the pre-#2702 lists in `ObjectGrid` / `InterfaceListPage` — never -listed `owner_id`. Because the framework's `applySystemFields` spreads its -injected system/audit/ownership fields to the FRONT of the field map and -`owner_id` is deliberately non-hidden and non-readonly (ownership is -reassignable), it leaked through as the leading, raw-id column on every object -without a declared list view (e.g. `showcase_invoice`), redundant with the -business `owner` (`Field.user`) column. - -Both paths now derive their columns through a single shared -`defaultListColumnsFromObject` helper that classifies system fields via the -`isSystemManagedField` helper from `@object-ui/types` (the same classifier -#2702 introduced) — branching on the spec `system` flag with a name-set -fallback that includes the ownership/tenancy FKs. Auto-derived lists lead with -business fields again and pick up future injected fields without editing a name -list. Closes #2777. diff --git a/.changeset/automations-flow-i18n.md b/.changeset/automations-flow-i18n.md deleted file mode 100644 index adb9bcfb4c..0000000000 --- a/.changeset/automations-flow-i18n.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -feat(app-shell): localize the automations flow designer & inspector (en-US + zh-CN) - -Comprehensive zh-CN localization of the metadata-admin automations surfaces — -the visual Flow designer, node/edge inspector, validation, and the shared editor -panels shown on the flow screen. Client-side per the platform's -`translateMetadataType` precedent; en-US is unchanged (every zh overlay falls -back to English, so unknown/plugin values are never hidden). - -- Flow designer: node palette (labels/hints/categories + Chinese search), canvas - chrome & tooltips, header pills incl. enum values, preview panels, run-history - & debug simulator, nested-region tray, and localized default labels for - newly-created nodes. -- Node & edge inspectors: config-field labels / help / options / column headers - for the full engine-published `configSchema` field set (loop - `indexVariable`/`maxIterations`, http `durable`/`signingSecret`, connector flat - ids, notify's config fields, …), keyed off the raw node type so aliased types - localize correctly. -- Structural + unknown-reference validation messages (canvas banner, Problems - panel, debug simulator) and the Problems-panel chrome. -- Generic `SchemaForm` enum-option / raw-field-label localization used on the - flow property form, plus the History / Audit / References / Layered-diff panels - and the force-save dialog shown on the flow screen. diff --git a/.changeset/chartconfig-remaining-inert-props.md b/.changeset/chartconfig-remaining-inert-props.md deleted file mode 100644 index de6ac9ff1b..0000000000 --- a/.changeset/chartconfig-remaining-inert-props.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@object-ui/plugin-charts": minor ---- - -feat(charts): honor `ChartAxis.stepSize`, `ChartConfig.description` and `ChartConfig.height` (framework#3752) - -The tail of the declared-≠-delivered sweep from framework#3729 / #2880. Three -`ChartConfig` props reached the renderer and did nothing: - -- **`ChartAxis.stepSize`** — Recharts has no "a tick every N units" prop - (`tickCount` is a hint it may ignore, `interval` is for categorical axes), so - honoring a step means handing it the tick array outright. `ticksFor` builds it - from the axis's own `min`/`max` where declared and from the plotted values - otherwise, so a step works with or without a pinned domain. A data-derived max - rounds UP to the next step (otherwise the topmost bar sits above the last - gridline and the axis reads as truncated); an explicit `max` clamps instead, - since a tick outside a pinned domain would be drawn outside the plot. A step - that would produce more than 200 ticks is refused rather than rendered — that - is a wrong config, and drawing it would hang the page instead of surfacing the - mistake. -- **`ChartConfig.description`** — the accessibility description. A chart is a - picture to a screen reader; the container now carries `role="img"` + - `aria-label`. Without a description it stays an ordinary div, because - stamping `role="img"` on an *unlabelled* graphic is worse than leaving one a - screen reader can skip. -- **`ChartConfig.height`** — was read only by the legacy `ChartBarRenderer`, not - by the advanced path that draws every real chart. Now applied to the chart - container as an inline style, which beats its default `h-[350px]` class. - -`height` and `description` ride on the shared container props, so they apply to -all eight chart families rather than one branch. diff --git a/.changeset/chatbot-and-inspector-i18n.md b/.changeset/chatbot-and-inspector-i18n.md deleted file mode 100644 index 3d20e5cd32..0000000000 --- a/.changeset/chatbot-and-inspector-i18n.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/plugin-chatbot": patch -"@object-ui/app-shell": patch ---- - -fix(i18n): unconditional Chinese in the chatbot confirm card and the field inspector (objectui#2884, objectui#2885) - -Two issues split out of the objectui#2871 survey because neither is a language -*branch* — both are copy that renders in Chinese for every user regardless of -locale. - -**objectui#2884 — the confirm-before-change card.** Heading, buttons, hint and -the verb column of each change row were Chinese literals, so an English user -read the whole confirm gate in Chinese. They now follow the same -prop-with-English-default convention the plan card already uses -(`changesTitleLabel`, `changesConfirmLabel`, `changeVerbLabels`, …), with the -console passing translated values from `console.ai.*`. - -The serious half was the outbound message. Clicking Confirm sent -`'确认修改,应用你刚才提议的改动。'` unconditionally — an English user's click -told the agent, in Chinese, to apply the changes, and the agent answered in -Chinese for the rest of the thread. That message now routes through the same -`convZh` (conversation-language) switch as `planApproveMessage`, so it matches -the language actually being spoken rather than the UI or a hard-coded literal. - -Note this is deliberately *not* "always send English": the repo already decided -outbound agent text follows the CONVERSATION, and the cloud confirm gate -(`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`) matches on approval -keywords. The Chinese string is unchanged, so that path is byte-for-byte what -the gate already accepted; `i18n.test.ts` now pins it against the mirrored gate -regex alongside the two plan messages. - -Also in this component: the error banner's `Response failed` / `Details` / -`Retry` were hard-coded English, and both it and the quota banner used a bare -`t(key)` that renders the raw key when the chat is mounted without an -`I18nProvider`. Both now use `useSafeTranslate`, so they degrade to English -instead of to `chatbotError.title`. The `「…」` corner brackets around the -target-app name are now neutral quotes. - -**objectui#2885 — the draft-field suffix.** `ObjectFieldInspector` appended a -bare `(草稿)` to draft objects in the lookup picker — the only Chinese literal -in a 1500-line file where the other 101 strings all go through `t(key, locale)`. -It now reads `engine.inspector.draftSuffix` from the Studio catalog. - -The 18 new keys were added to all ten locale packs, so the objectui#2872 part -(a) gap held at 469/471 rather than widening. diff --git a/.changeset/cloud-connection-panel-i18n.md b/.changeset/cloud-connection-panel-i18n.md deleted file mode 100644 index ddcffa7c25..0000000000 --- a/.changeset/cloud-connection-panel-i18n.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/app-shell": patch ---- - -fix(cloud-connection): localize the Cloud Connection panel (objectstack#3589 follow-up) - -`CloudConnectionPanel` — the `cloud-connection:panel` SDUI widget that is the -entire body of the Cloud Connection Setup page — had no i18n at all: no -`@object-ui/i18n` import, and no `cloudConnection` namespace in any of the ten -built-in locale packs. Its siblings on neighbouring pages -(`marketplace:installed-list`, `mcp:connect-agent`) were already fully -localized, so this one page rendered a translated header above an English body -once the framework-side `page:header` resolution landed. - -- New `cloudConnection` namespace in all ten packs (en, zh, ja, ko, de, fr, es, - pt, ru, ar), matching the coverage its sibling namespaces already had. Covers - every phase of the device-code flow: checking, error + retry, waiting - (approval prompt, user code, copy), bound (connection detail labels), and - unbound (call to action). -- The three hard-coded failure messages (expired request, bind failure, device - code request failure) are translated where they are raised, not where they - are rendered, since they are stored in component state. -- The "code is pre-filled…" line was one sentence stitched together across JSX - with a conditional tail and a bare `'.'`. It is now two self-contained - strings, so a translator never receives a dangling clause whose word order - they cannot change. -- The `bound_at` timestamp now formats with the active UI language rather than - the browser default, matching the surrounding copy. - -Also adds a locale-parity test asserting the `cloudConnection` key set is -identical across all ten packs — partial coverage degrades quietly, because -i18next falls back to `en` and the result merely looks half-translated. diff --git a/.changeset/column-summary-spec-vocabulary.md b/.changeset/column-summary-spec-vocabulary.md deleted file mode 100644 index 9f3e35e73b..0000000000 --- a/.changeset/column-summary-spec-vocabulary.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@object-ui/plugin-grid": minor -"@object-ui/types": minor -"@object-ui/react": minor ---- - -feat(grid): compute all eleven spec column summary aggregations (#2890) - -`ColumnSummarySchema` accepts eleven aggregation names; `useColumnSummary` computed -five. The other six — `none`, `count_empty`, `count_filled`, `count_unique`, -`percent_empty`, `percent_filled` — passed validation at authoring time and then -rendered a blank footer cell, with no error raised on either side. - -The computation now splits into two families. Count and percent read *raw* cell -values, before the numeric parse, so they work on text, select and lookup columns and -a value that does not parse as a number still counts as a filled row; a cell is empty -when it is `null`, `undefined`, `""` or an empty array. `sum`/`avg`/`min`/`max` keep -the existing numeric parse and column formatting. - -Two behavior changes follow from the enum carrying both `count` and `count_filled`, -which cannot mean the same thing: - -- `count` is now every row; `count_filled` is the non-empty variant. Only a column - whose values are all empty renders differently than before. -- a zero count renders `Empty: 0` instead of collapsing to a blank cell. - -Column currency/percent formatting is gated to the numeric family, so `count_unique` -on a currency column reads `Unique: 3` and not `$3.00`. `none` and unrecognized names -skip the entry entirely, so a view whose columns all opt out renders no footer row. - -`ListColumnSchema`'s objectui-local `{ type, field }` arm now takes its vocabulary -from `SpecColumnSummarySchema` by reference — it was stuck at the same five names, -which left the per-column `field` override unavailable for the six new aggregations. - -A parity test asserts the renderer's supported set equals the spec enum in both -directions: a spec name the renderer omits is the bug above, and a renderer name the -spec omits would be local dialect (Commandment #0). - -**Removed:** `useColumnSummary` from `@object-ui/react`. It was a second, unrelated -hook of the same name with no callers — a different API, a comment claiming it -implemented spec v2.0.7, and a `distinct` aggregation that is not in the spec -vocabulary at all (the spec calls it `count_unique`). Use `useColumnSummary` from -`@object-ui/plugin-grid`, which implements the spec enum. diff --git a/.changeset/command-palette-global-search.md b/.changeset/command-palette-global-search.md deleted file mode 100644 index b4e89a0972..0000000000 --- a/.changeset/command-palette-global-search.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/data-objectstack": patch -"@object-ui/react": patch -"@object-ui/app-shell": patch -"@object-ui/types": patch ---- - -Command palette (⌘K) now surfaces record search hits from the platform's global -search endpoint (`GET /api/v1/search`). - -Previously the palette only ran a per-object `find({ $search })` fanout (the -metadata-driven ADR-0061 search), which misses records that only the global -search index knows about — so typing a well-known record name returned no -records even though `/api/v1/search` served them. `ObjectStackAdapter` now -exposes a `searchAll(query, { limit, objects })` method that calls the unified -endpoint, `useRecordSearch` prefers it when present (falling back to the fanout -otherwise), and the palette renders the resulting record hits grouped by object. diff --git a/.changeset/complete-locale-backfill.md b/.changeset/complete-locale-backfill.md deleted file mode 100644 index df1fcf840a..0000000000 --- a/.changeset/complete-locale-backfill.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -feat(i18n): complete the locale backfill — all ten packs reach full key parity (objectui#2872) - -Translates the remaining **275 keys × 8 packs = 2,200 strings**, closing -objectui#2872. The largest namespaces are `grid` (101, mostly the import -wizard), `gantt` (58) and `dashboard` (25), plus a long tail across `list`, -`auth`, `fields`, `marketplace`, `capability` and nine others. - -Every pack is now at parity with `en`: **2,495 of 2,499 keys**, zero keys that -`en` lacks. The four-key remainder is the outbound-message set, absent by -design so `t()` falls through to English and the cloud confirm gate keeps -recognising it — `outbound-agent-messages.test.ts` owns that invariant. - -**P3 is now enforceable.** `high-frequency-namespace-parity.test.ts` was scoped -to four namespaces because full parity would have been a permanently red build. -That restriction is obsolete, so it is replaced by -`all-locales-key-parity.test.ts`, which asserts: - -- every pack defines every `en` key; -- no pack defines a key `en` lacks (objectui#2872 part b was 74 keys of exactly - this, hidden behind a component-private fallback); -- **placeholders match `en` per string** — both `{{count}}` and the single-brace - `{count}` form, which two `gantt.autoScheduleDlg.*` keys use on purpose - because their call site does a literal `.replace('{count}', …)` rather than - i18next interpolation. A translation that drops a placeholder renders a - sentence with a hole in it and no error, so this is checked mechanically - rather than by eye. - -All three assertions were mutation-tested, including the single-brace form. - -### A bug the test suite could not have caught - -The first merge pass produced **duplicate keys** in four packs: the key list is -the union of what is missing across all eight, but the insert ran -unconditionally, so packs that already had `detail.created` / `detail.updated` -got a second copy. Every test still passed — at runtime the later property -simply wins, so the parity check saw a perfectly consistent object. - -`tsc` caught it as TS1117 during `turbo build`. ESLint does not flag it, and a -runtime test *cannot* — the duplicate is already collapsed before JS sees the -object. The compiler is the only possible guard here, and CI runs it. The merge -script now filters per pack against what that pack actually defines. - -### Translation quality - -Model-generated, and dense domain terminology (Gantt dependency types, the -import wizard's upsert/match-field vocabulary) is exactly where that is -weakest. This was raised before starting and the work was requested anyway, so -it ships as a **reviewable first draft, not a finished localization** — native -review is still worthwhile. What *is* verified mechanically: key parity in both -directions, placeholder shape per string, and that no outbound agent message -was translated. diff --git a/.changeset/confirm-gate-english-clause.md b/.changeset/confirm-gate-english-clause.md deleted file mode 100644 index 17802e0aac..0000000000 --- a/.changeset/confirm-gate-english-clause.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/plugin-chatbot": patch -"@object-ui/app-shell": patch ---- - -fix(i18n): the change card's Confirm button sent text the cloud gate does not accept - -The English `console.ai.changesConfirmMessage` was -`"Confirm the changes — apply what you just proposed."`. The cloud confirm gate -(`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`) recognises -`apply (this|the) change` — **not** "apply what". So the message failed the -gate, and failing the gate is silent: the agent re-proposes instead of applying, -and the Confirm button on the change card simply looks inert. - -This affected English conversations **and all eight locales that fall back to -English** for that key. It is now -`"Confirm — apply the change you just proposed."` — singular "the change", so it -still matches if the gate ever tightens to a word boundary. The Chinese string -was always fine (`确认修改` hits the 确认-anchored clause) and is unchanged. - -The same literal lives in four places — the locale pack, the -`ChatbotEnhanced` prop default, its doc comment, and the `AiChatPage` -`defaultValue` — and all four are updated together. - -**Why the existing guard missed it.** `i18n.test.ts` mirrored only the *Chinese* -clause of `APPROVAL_RE`; the English half was reduced to "starts with Confirm, -contains apply" because nothing in this repo could see the real pattern. That -weaker assertion passed against a string the gate rejected — the guard was -green and the feature was broken. - -The mirror is now **verbatim, both clauses**, and drives an `it.each` over every -outbound approval message in both `zh` and `en`. Two supporting tests keep it -honest: one asserting the gate stays narrow (a plain build request like -"帮我搭建一个 CRM" must NOT read as approval), and one asserting -`planAnswerMessage` does *not* match — it answers a structure question and must -never read as blanket approval. - -The mirror is duplicated across a repo boundary by necessity (objectui cannot -import from cloud); the comment says so, so the next person changing -`APPROVAL_RE` knows to update it here too. diff --git a/.changeset/console-delegated-admin-role.md b/.changeset/console-delegated-admin-role.md deleted file mode 100644 index 5a90487447..0000000000 --- a/.changeset/console-delegated-admin-role.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@object-ui/auth": minor -"@object-ui/app-shell": minor ---- - -feat(console): make the `delegated_admin` org role reachable, and narrow both role pickers to what the server will accept (framework#3697) - -The framework registered a fourth organization role — `delegated_admin`, the -grade that may reach `/organization/invite-member` **without** being an org -admin, which is what finally gives ADR-0105 D8's scope-bounded issuance gate a -caller. objectui#2868 already shipped the placement half of that UX (units and -positions narrowed by `describeDelegableScope()`), but the console could not -select the role in the first place: `MembersPage` and `InviteMemberDialog` each -inlined `type Role = 'owner' | 'admin' | 'member'`, so the capability the -framework grew was unreachable from either screen. - -**One vocabulary, not two.** The role names, labels and narrowing rules now live -in `@object-ui/auth`'s new `org-roles` module (`ORG_ROLES`, `ORG_ROLE_LABELS`, -`orgRoleGrade`, `invitableOrgRoles`, `assignableOrgRoles`) and both screens -consume it. Note this list still **mirrors** the server rather than deriving -from it — `/auth/config` publishes feature flags but no role vocabulary, so -there is no surface to read; objectstack-ai/objectstack#3723 tracks making one -list the source for all of them. Until then a server-side role addition means -one console edit instead of two. - -**The pickers now narrow, the way the placement picker already does.** Both -mirror a *different* server gate, and offering an option the server would refuse -is the failure they prevent: - -- **Invite role** ← the framework's `beforeCreateInvitation` role cap: never - above the issuer's own grade, and an issuer below admin grade may invite as - `member` only. A `delegated_admin` who picked "Admin" would have been refused - with a 403; that option is simply no longer offered. -- **Change role** ← better-auth's `update-member-role` route: it requires the - `member:["update"]` permission (owner/admin only — `delegated_admin` is built - from `memberAc` and holds `member: []`), and only an owner may set `owner` or - re-role an existing owner. An actor who may re-role nobody now gets no items - instead of three that would 403. - -Narrowing is convenience, not the boundary — the server re-checks every one of -these — and it fails toward *less*: an unresolved membership offers `member` -alone on invite, and nothing on re-role. - -An ordinary invitation is unchanged: with the default role and no placement, the -request body is byte-identical to before. - -Note for translators: `organization.roles.*` has never been defined in any -locale bundle — all four labels (owner/admin/member included) resolve through -their `defaultValue` English fallback. The new role follows the same pattern -rather than being the only localized one. diff --git a/.changeset/d8-invitation-placement-ux.md b/.changeset/d8-invitation-placement-ux.md deleted file mode 100644 index 68a5d9e071..0000000000 --- a/.changeset/d8-invitation-placement-ux.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/auth": minor -"@object-ui/app-shell": minor ---- - -feat(console): scoped-invitation placement — invite someone straight into a -business unit and positions (framework ADR-0105 D8) - -An invitation may now carry PLACEMENT INTENT: the business unit the invitee -lands in and the positions they are assigned when they accept. A plant admin's -invitee arrives already in the right unit and role instead of waiting on a -platform admin to finish the job by hand. - -- `@object-ui/auth`: `inviteMember` accepts optional `businessUnitId` / - `positions` (passed through better-auth's invitation `additionalFields`), and - a new `describeDelegableScope()` reads - `GET /api/v1/security/my-delegable-scope`. -- `InviteMemberDialog`: an optional "Placement" section listing **only** the - units the issuer may place into and the positions they may hand out. - Positions appear once a unit is chosen — an unanchored assignment is refused - by the server, so offering it first would mislead. - -The narrowing is convenience, not the boundary: the server authorizes the pair -against the ISSUER's `adminScope` (ADR-0090 D12) at issuance and rejects the -whole invitation when it is out of scope. Accordingly the section is **hidden** -whenever the caller has no delegable authority, or the deployment exposes no -delegated-administration runtime at all (the endpoint answers 501 ⇒ `null`) — -never a form the server would refuse. An ordinary invitation is unchanged: with -no placement chosen, the request body is byte-identical to before. diff --git a/.changeset/dashboard-filter-context-tokens.md b/.changeset/dashboard-filter-context-tokens.md deleted file mode 100644 index a44c150cd6..0000000000 --- a/.changeset/dashboard-filter-context-tokens.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -"@object-ui/core": patch -"@object-ui/react": patch -"@object-ui/plugin-dashboard": patch -"@object-ui/plugin-charts": patch -"@object-ui/app-shell": patch ---- - -fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework #3574) - -A dashboard widget filtered on `{current_user_id}` rendered `0`. The token -reached SQL as a literal, matched no row, and nothing was logged on the client -or the server — a silent zero that reads as "you have no work" rather than -"this filter did not resolve". The same token in a list-view filter resolved -correctly, so a user-scoped list and a user-scoped widget over the same data -disagreed. - -There was no shared resolver. Three ad-hoc implementations had grown up -independently — `ObjectView` for list views, `ObjectDataPage` for URL filter -triples, `NavigationRenderer` for hrefs — and each understood only the filter -shape its own surface used. `ObjectView`'s opened with -`if (!Array.isArray(filter)) return filter`, so it could not have been reused -by dashboard widgets even in principle: widget filters are MongoDB-style -objects. Widgets therefore got no resolution at all — `DatasetWidget` called -`resolveDateMacros` and nothing else, which is why `{today}` worked in a widget -and `{current_user_id}` silently did not. - -- **`@object-ui/core`** — new `utils/filter-tokens.ts` with - `resolveContextTokens` and `resolveFilterPlaceholders`. The latter expands - *every* placeholder vocabulary in one call and is what surfaces should use; - resolving only some of them is the whole defect. The walk handles arrays and - plain objects uniformly, so one resolver covers both platform filter shapes. -- **`@object-ui/react`** — new `FilterScopeProvider` / `useFilterScope`. The - renderer packages deliberately do not depend on `@object-ui/auth`, so the - shell supplies the session values. This is a separate context from - `PredicateScopeContext`, which is the expression evaluation scope and carries - no organization. -- **`@object-ui/plugin-dashboard` / `@object-ui/plugin-charts`** — all six - widgets that previously resolved date macros only now resolve both - vocabularies: `DatasetWidget`, `ObjectMetricWidget`, `ObjectDataTable`, - `ObjectPivotTable`, and `ObjectChart` (dataset-bound and inline paths). The - chart's `compareTo` comparison filter gets the session pass too — otherwise - the overlay series silently ignored the owner clause the primary series - honoured. -- **`@object-ui/app-shell`** — `ObjectView`'s local `substituteFilterTokens` - and `ObjectDataPage`'s inline `=== '{current_user_id}'` ternary now delegate - to the shared resolver, so both also gain `{current_org_id}` and date macros. - Two of the three ad-hoc implementations are gone rather than joined by a - fourth. - -An unresolvable token is left intact rather than dropped: leaving it yields an -empty result, whereas dropping the clause would *widen* the result set and show -a signed-out viewer everyone's data. It is no longer silent — the resolver -warns, naming the token, and suggests the intended spelling for known -near-misses (`{current_user}`, `{user_id}`, `{organization_id}`). Authoring-time -enforcement lands separately as `filter-token-unknown` in `@objectstack/lint`. diff --git a/.changeset/data-table-sticky-action-header.md b/.changeset/data-table-sticky-action-header.md deleted file mode 100644 index 61ec6e0028..0000000000 --- a/.changeset/data-table-sticky-action-header.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@object-ui/components": patch ---- - -fix(data-table): keep the right-pinned action column HEADER sticky on horizontal scroll (objectui#2784) - -The row-actions column is pinned to the right edge by injecting `sticky right-0` -into the column's `className`, which reaches both the body cells and the header -cell. Body cells stayed pinned, but the header cell unconditionally appended a -`relative` position utility (it anchors the column-resize handle) — and since -`cn` is `tailwind-merge`, the later `relative` won over the injected `sticky`. -So the "操作" title scrolled away while its body cells stayed frozen. - -The header now detects a right-pinned column (its `className` carries -`sticky` + `right-0`), skips `relative` for it (a sticky cell is already its own -positioning context, so the `absolute` resize handle still anchors correctly), -and re-asserts `sticky right-0 z-20` after `col.className` so tailwind-merge -keeps the pin and it stacks above the body's pinned cells (z-10). Left-frozen -columns, the resize handle, and non-pinned columns are unaffected. diff --git a/.changeset/dataset-widget-query-options.md b/.changeset/dataset-widget-query-options.md deleted file mode 100644 index dee90aabcb..0000000000 --- a/.changeset/dataset-widget-query-options.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@object-ui/core": patch -"@object-ui/plugin-charts": patch -"@object-ui/plugin-dashboard": patch ---- - -fix(dashboard,charts): send widget `dateGranularity`/`sortBy`/`limit` to the query, and give funnels a real stage order (framework#3588) - -`DatasetWidget` never read `widget.options`. Four keys an author writes there -change the query the server compiles, so a widget declaring -`options: { dateGranularity: 'month' }` grouped by the raw timestamp and drew -one bar per record, and `sortBy`/`sortOrder` produced no ordering at all. - -- `DatasetWidget` lowers `options.dateGranularity`, `options.sortBy` + - `options.sortOrder`, and `options.limit` into the `DatasetSelection` it posts. - A `sortBy` naming something the widget does not project is dropped rather than - sent, so a stale sort key left by an edit degrades to "unordered" instead of - failing the widget against the server's stricter validation. These keys also - join the refetch signature, so editing one in the designer refetches instead - of re-rendering the previous grid. -- Funnel stages follow a **declared order**. `AdvancedChartImpl` sorted funnel - segments by value descending, unconditionally — which overrode any server - ordering and rendered a sales pipeline as a tidy narrowing shape whatever the - stages' real sequence, hiding the very anomaly (a bulge at Proposal) the chart - exists to show. It now honours a `categoryOrder`, which `DatasetWidget` - derives from the dimension field's picklist option order — the pipeline order - an author already declared on the object — or from an explicit - `options.stageOrder`. With no declared order the value-descending default is - unchanged, and a category missing from the order is kept (after the declared - ones), never dropped. -- New `@object-ui/core` helpers `buildCategoryOrder` / `buildCategoryRank`, - keyed by both the stored value and the display label like the existing - `buildOptionColorMap`, so ordering works whether or not the server resolved - the dimension's labels. - -Requires the framework-side fix in objectstack#3588 for the selection keys to -take effect server-side. diff --git a/.changeset/declared-actions-bar-chrome-i18n.md b/.changeset/declared-actions-bar-chrome-i18n.md deleted file mode 100644 index 9208e8b492..0000000000 --- a/.changeset/declared-actions-bar-chrome-i18n.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/i18n": patch ---- - -fix(app-shell): localize the two `DeclaredActionsBar` strings that bypassed i18n (objectui#2762 P0-3) - -The declared action *labels* resolve through `useObjectLabel`, so a zh-CN -workspace got 通过 / 拒绝 buttons — sitting inside a toolbar whose accessible -name was the English literal `'Actions'`, above decision-output fields whose -help text read `Handed to the flow as a decision output.` Both strings are -authored by the bar itself rather than by metadata, and both skipped the locale -bundle entirely. - -- `aria-label` now uses the existing `common.actions` key (a host-supplied - `label` still wins). -- The decision-output help text moves to new `actions.decisionOutput.help` / - `.helpMultiValue` keys, added across all ten shipped locales. - -Worth being precise about why the help text needed fixing at all, since the -runtime *does* localize action params: `useConsoleActionRuntime` runs every -param through `actionParamText`, but these params are synthesized here from the -record's `decision_output_defs`, so their key path (`outputs.`) is dynamic -and no `_actions..params.*` bundle entry can ever match it. The -fallback is not a rare path — it is the only path, which is why the English -survived. - -Not fixed, and deliberately: a decision output that arrives without a `label` -still renders a title-cased version of its machine key. That derived text -mirrors the framework's `humanizeFieldPath` convention, and the real fix is the -backend declaring the label — a client-side bundle cannot key off a dynamic -output name. diff --git a/.changeset/detail-form-effective-ops.md b/.changeset/detail-form-effective-ops.md deleted file mode 100644 index 3761b5eb07..0000000000 --- a/.changeset/detail-form-effective-ops.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/app-shell": minor -"@object-ui/plugin-detail": minor -"@object-ui/plugin-form": minor ---- - -feat: gate detail/form edit & delete on the server's effective operation set (#3546) - -PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView / -ObjectGrid Export) to the server-resolved effective API operation set -(`/me/permissions` `apiOperations`, intersected via -`resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form** -surfaces still gated edit/delete on the bucket + `userActions` alone. This -extends the same intersection to them, so the record page and its forms never -offer an operation the server would 405. - -- **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same - optional second argument as `resolveCrudAffordances` — inline-edit is now - additionally ANDed with the server allowing `update`. -- **app-shell** `RecordDetailView` threads the object's effective operations into - the synthesized Edit/Delete header actions and the record-body inline-edit - gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child** - object's Create/Edit/Delete handlers with that child's own effective set. -- **plugin-detail** `record:details` ANDs its inline-edit affordance with the - object's effective `update`. -- **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages - when the server denies `update` (edit mode) / `create` (create mode). - -Backward-compatible: a missing effective set (unrestricted object, older -backend, or no `PermissionProvider`) leaves the resolved affordance untouched — -the bucket/`userActions` decision wins, exactly as today. Layers on top of the -existing per-object `check('edit')` / `check('delete')` permission gates -(intersection, never union). diff --git a/.changeset/drop-capabilities-descriptor-reexports.md b/.changeset/drop-capabilities-descriptor-reexports.md deleted file mode 100644 index 83921922b5..0000000000 --- a/.changeset/drop-capabilities-descriptor-reexports.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/types": minor ---- - -feat(types)!: drop the `ObjectStack/ObjectOS/ObjectQL/ObjectUI Capabilities` re-exports (framework capabilities-descriptor prune) - -Upstream `@objectstack/spec` removed the dead static capability-descriptor -cluster (`ObjectStackCapabilitiesSchema` / `ObjectOSCapabilitiesSchema` / -`ObjectQLCapabilitiesSchema` / `ObjectUICapabilitiesSchema` + their types) — -a never-wired fixed-boolean self-portrait whose defaults contradicted the -live platform (FLS/RLS/audit all `default(false)` while actually enforced). -This drops the `@object-ui/types` re-exports of those symbols. - -**Migration**: discover real runtime capabilities at runtime, not from a -static schema — `GET /api/v1/discovery` (dynamic `capabilities` record with -declared === enforced discipline) and the `/.well-known` contract -(`WellKnownCapabilitiesSchema` from `@objectstack/spec/api`). No replacement -re-export. diff --git a/.changeset/drop-dead-spec-bridge-page-dashboard.md b/.changeset/drop-dead-spec-bridge-page-dashboard.md deleted file mode 100644 index 8c883e4925..0000000000 --- a/.changeset/drop-dead-spec-bridge-page-dashboard.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/react": major ---- - -refactor(spec-bridge): remove the dead page/dashboard bridges (#1892) - -`SpecBridge`'s `page` and `dashboard` bridges — `bridgePage`, `bridgeDashboard`, -and the `SpecBridge#transformPage` / `#transformDashboard` methods — had no -runtime consumer. Pages render through their own renderer and dashboards -through `DashboardView → DashboardRenderer → DatasetWidget` (ADR-0021); neither -path routes through `SpecBridge`. The dashboard bridge's input shape -(`object` / `categoryField` / `valueField` / `aggregate`) is the pre-ADR-0021 -widget model, which the strict `DashboardWidgetSchema` now rejects — so the -bridge could not receive a spec-valid dashboard even in principle. - -Flagged dead by the metadata-liveness audit (framework #1878 / #1892). The -`list` and `form` bridges are unaffected and remain the live authoring path. - -BREAKING CHANGE: the public exports `bridgePage`, `bridgeDashboard`, and the -`SpecBridge#transformPage` / `#transformDashboard` methods are removed. There -is no replacement — render pages and dashboards through their renderers -(`DashboardRenderer` / the page renderer) directly. diff --git a/.changeset/drop-skill-permissions-preview.md b/.changeset/drop-skill-permissions-preview.md deleted file mode 100644 index d19b3b0aff..0000000000 --- a/.changeset/drop-skill-permissions-preview.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(metadata-admin): drop the SkillPreview "Required Permissions" panel (framework#3686) - -Upstream removed `SkillSchema.permissions` — skill invocation was never gated by -it. Showing a "Required Permissions" section (and a "N required perms" header -pill) for an unenforced list taught the wrong model: access is gated at the -AGENT (`access`/`permissions`, enforced at the chat route) or on the underlying -actions the skill's tools call. diff --git a/.changeset/effective-api-operations-ui.md b/.changeset/effective-api-operations-ui.md deleted file mode 100644 index d44d899ae2..0000000000 --- a/.changeset/effective-api-operations-ui.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/permissions": minor -"@object-ui/plugin-grid": minor -"@object-ui/plugin-list": minor -"@object-ui/app-shell": minor -"@object-ui/i18n": minor ---- - -feat: render the server's effective API operation set (#3391 PR-4) - -The frontend now consumes the per-object **effective API operation set** the -server resolves (from `/me/permissions` `apiOperations`, framework #3391) — -never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what -the server will actually admit, and a 405 import refusal shows a dedicated -message instead of silently falling back. - -- **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional - second argument intersects each affordance bit with its API operation - (create/import→create/import, edit→update, delete→delete, exportCsv→export). - Omitting it (old backend / no effective set) leaves affordances unchanged. -- **permissions** — `/me/permissions` response carries per-object - `apiOperations`; `PermissionContextValue.getObjectApiOperations(object)` - exposes it (undefined when absent → callers keep current behavior); `check()` - maps `import→allowCreate`, `export→allowRead`. -- **app-shell** `ObjectView` intersects its toolbar affordances with the object's - effective operations (Import); the platform-admin identity-import bypass is - unaffected. -- **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export - button (and export handler) on effective `export`; `plugin-grid` gains the - `@object-ui/permissions` workspace dependency. -- **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED` - import refusal is detected by a new `isImportNotAllowed` predicate at every - catch site (async, sync, dry-run) and STOPS with a dedicated - `grid.import.notAllowed` message (10 locales + fallback dict) — it never falls - back to the sync/legacy path (which 405s too), distinct from the 404 - route-absent fallback. - -Backward-compatible: a missing effective set (unrestricted object, older -backend, or no permission provider) preserves the current default-allow -behavior everywhere. diff --git a/.changeset/file-as-reference-adoption.md b/.changeset/file-as-reference-adoption.md deleted file mode 100644 index 611cb22ecb..0000000000 --- a/.changeset/file-as-reference-adoption.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -"@object-ui/fields": minor -"@object-ui/app-shell": patch ---- - -feat(fields): adopt the file-as-reference value shape (ObjectStack ADR-0104 D3 wave 2) - -A `file`/`image` field value now reaches the UI in one of three forms, and the -rules for reading them live in one place — `@object-ui/fields`' new -`file-value` module — instead of being re-derived in each widget: - -1. **Reference** — a bare `sys_file` id string, what the backend stores once - file-as-reference is adopted. -2. **Expanded** — `{ id, name, size, mimeType, url }`, what the read path - returns after resolving a reference. -3. **Legacy inline blob** — `{ file_id?, name, original_name, size, mime_type, - url }`, the pre-reference shape this package used to build itself. - -**The casing split is the bug this fixes.** The expanded form carries -`mimeType`; the legacy blob carries `mime_type`. `FileField`, `FileCell` and -`ImageField` all read only `mime_type`, so the moment a backend starts returning -the expanded form they stop recognising images — thumbnails silently degrade to -a generic file icon, with nothing pointing at a value shape as the cause. -`readFileValue()` accepts both. - -**Uploads now submit the reference form** — the bare `sys_file` id — when the -upload adapter surfaced one, falling back to the legacy blob when it did not -(the object-URL fallback adapter, or a backend predating file-as-reference). The -same build therefore works against both. Action params already POSTed a bare -fileId; record field values now use the same contract, and -`serializeParamValues` shares the `fileIdOf()` extractor so the two surfaces -cannot drift on what counts as an id. - -Because a bare id carries no name or URL, each widget remembers the display -details of files it just uploaded, keyed by id, so an upload renders immediately -rather than showing a bare token until the next read enriches it. diff --git a/.changeset/flow-actions-dispatch-everywhere.md b/.changeset/flow-actions-dispatch-everywhere.md deleted file mode 100644 index d2bf45c80d..0000000000 --- a/.changeset/flow-actions-dispatch-everywhere.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/plugin-dashboard": patch ---- - -fix(console): dispatch flow actions from every surface, and cover the screen-flow round trip (framework#3528) - -The resume half of screen flows is fixed; these are the two launch-side holes -found while mapping every path that dispatches a `type: 'flow'` action — on -both, a screen flow could not even be started. - -- **plugin-dashboard** — a dashboard header action only dispatched when its type - was `modal` or `script`. `flow` (and `api` / `form` / `navigation`) fell - through to `console.warn("Unknown header actionType")` and did nothing at all. - The click handler now routes everything that is not a raw `url` navigation - through the ActionRunner, which owns the type registry; there is nothing for - the renderer to second-guess. -- **app-shell** — the console-root `` was mounted with no - `handlers` map. It exists to give every field widget a modal handler, but an - `ActionProvider` also decides what a `useAction()` consumer *below* it can - dispatch, so any `action:button` outside ObjectView / RecordDetailView / - PageView / DeclaredActionsBar bound to a runner that could only open modals: - a `flow` action there failed with "Flow handler not registered", and `api` / - `script` were equally dead. The root now carries the shared console runtime's - api / flow / script handlers plus its confirm / param / result / screen-flow - dialogs. `modal` deliberately stays on the client-side `useActionModal` - handler — registering it in `handlers` would take precedence over `onModal` - and reroute the inline-create affordance to `/api/v1/actions/...`. - -Both changes ship with regression tests that were verified to fail without them. -Also adds the first coverage of the screen-flow seam itself, which had none: - -- `FlowRunner.suspense.test.tsx` — a lazily-loaded screen body must not unwind - past the dialog. Reproduces the real shape (lazy body, route-level boundary - above the host, host state that must survive) and fails against the - pre-boundary runner, which is how a paused run's screen used to vanish before - it could be submitted. -- `e2e/live/screen-flow.spec.ts` — the live round trip: a row flow action - triggers the run, the paused screen renders, Submit POSTs to - `/automation/{flow}/runs/{runId}/resume` with the collected values, and the - flow's downstream `update_record` shows up in the list. The unit tests stub - the runner out of the action runtime and the runner's own tests feed it a - screen directly, so trigger → dialog → resume → refresh was previously only - ever exercised by hand. diff --git a/.changeset/flow-created-or-updated-trigger.md b/.changeset/flow-created-or-updated-trigger.md deleted file mode 100644 index 2ac8e520be..0000000000 --- a/.changeset/flow-created-or-updated-trigger.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -feat(app-shell): Studio flow start node offers a "Record created or updated" trigger (#3427) - -The record-change trigger now supports `record-after-write` (create OR update in -one flow), so the flow designer's start-node trigger picker offers a "Record -created or updated" option. Selecting it shows the Object and Entry-condition -fields, and the scope resolver puts both `record` and `previous` in scope for it -(`previous == null` is how an author branches the create leg) — mirroring the -runtime binding that fires the flow on both insert and update. diff --git a/.changeset/flow-notify-studio-node.md b/.changeset/flow-notify-studio-node.md deleted file mode 100644 index 224feafdba..0000000000 --- a/.changeset/flow-notify-studio-node.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -feat(studio): first-class `notify` flow node in the Studio palette + inspector - -The `notify` flow node (ADR-0012 — outbound notification via the messaging -service) is a live built-in with a server descriptor, but Studio had no static -palette entry or config editor for it: `fieldsForNodeType('notify')` returned -`[]`, so it was only authorable by hand-editing JSON or when the running engine -happened to publish its descriptor (framework#1878 / framework#1895). - -- Added `notify` to `NODE_PALETTE` (Integration), with a Bell icon and the - integration tone, canvas category, and a sensible default-config seed - (`channels: ['inbox']`). -- Added a `notify` entry to `FLOW_NODE_CONFIG` mirroring the built-in node's - descriptor keys: `recipients`/`channels` (stringList), `title`, `message` - (textarea), `topic`, `severity` (select info/warning/critical), and the - click-through target (`sourceObject`/`sourceId`/`url`) — all written under - `node.config`. - -Closes the last item of the designer-authoring-gaps issue (framework#1895). -Unit + DOM tested (palette entry, config field kinds/paths, no inspector -regression). A browser dogfood pass of authoring a notify node end-to-end is -recommended before merge. diff --git a/.changeset/flow-object-list-commit-in-render.md b/.changeset/flow-object-list-commit-in-render.md deleted file mode 100644 index 4b30bfbf7e..0000000000 --- a/.changeset/flow-object-list-commit-in-render.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(app-shell): stop the flow-node repeater from committing during render (#2838) - -Operating any `commitCell`-backed control in a flow node's objectList repeater — -checkbox, select cell, record lookup, nested list, remove-row — logged a React -warning: - -``` -Cannot update a component (`MetadataResourceEditPageImpl`) while rendering a -different component (`FlowObjectListField`). -``` - -`commitCell` and `removeRow` called `flush()` (which calls the parent's -`onCommit`) from inside their `setRows` updater. React runs updaters during the -render phase, so the parent's `setState` landed mid-render — the exact pattern -React flags. React only warns once per component pair, so whichever control the -author touched first "claimed" the warning and every other one looked innocent. - -The handler now raises a commit-intent flag and leaves the updater pure; an -effect flushes after commit. Because the effect reads the rows React actually -applied, a commit no longer risks publishing a stale snapshot when another -update is already queued (typing in a cell and then hitting the row's ✕ in the -same tick). - -The plain suites missed this because React computes an updater eagerly when the -fiber has no pending work — that path runs it in the handler and hides the -warning — and because an `onCommit: vi.fn()` parent takes no update at all. The -new regression test reproduces both conditions. diff --git a/.changeset/flow-runs-screen-flow-resume.md b/.changeset/flow-runs-screen-flow-resume.md deleted file mode 100644 index 9059011d50..0000000000 --- a/.changeset/flow-runs-screen-flow-resume.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -"@object-ui/console": patch -"@object-ui/app-shell": patch ---- - -fix(console): let a screen flow be completed from the developer Flow Runs page (framework#3528) - -Developer → Flow Runs triggers a flow and renders the result. For a **screen** -flow that result is not a result — it is `{ status: 'paused', runId, screen }`, -and the run sits suspended until something posts to its resume endpoint. The -panel dumped that envelope as JSON and stopped: no screen, no Submit, no resume -call. Every test run of a screen flow left an orphaned `paused` row in Recent -Runs, and there was no way to drive one to completion from this surface. - -- **console** — a paused test run now opens the same `FlowRunner` the record and - list surfaces use, so the screen renders for real (flat fields, multi-step - wizards, and `object-form` steps with their master-detail grids) and Submit - posts to `/automation/:flow/runs/:runId/resume`. Dismissing the runner no - longer strands the run: the pause is durable, so the panel keeps a "Continue - run" affordance to reopen the pending screen. `paused` also gets its own - status badge instead of falling through to the unknown-status style. -- **app-shell** — `FlowRunner` (and its `ScreenFlowState` / `ScreenSpec` types) - is now exported from the package so surfaces outside `views/` can mount the - one screen-flow runner rather than reimplementing it. -- **app-shell** — `FlowRunner` now wraps the screen body in its own `` - boundary. An `object-form` step mounts `ObjectForm`, whose field widgets are - lazy; that suspension used to unwind to the *host's* nearest boundary, and on - a surface whose nearest boundary is the route-level one, React swapped the - whole page for the fallback and remounted it — destroying the host's state - along with this dialog. The screen vanished before it could be filled in and - the run stayed paused with no resume call, which is exactly the "Submit does - nothing" shape. Reproduced on the Flow Runs page and fixed at the source, so - every host that mounts the runner is covered. -- **app-shell** — a screen payload without `fields` no longer throws. `fields` - is optional on the wire (a message-only screen, or an `object-form` step from - a node executor that omits it), but `FlowRunner`/`ScreenView` read it - unguarded and blew up as the dialog mounted. Reads now go through a - `screenFields()` helper; the design-time builder keeps its exhaustive shape. diff --git a/.changeset/flow-runs-step-warnings.md b/.changeset/flow-runs-step-warnings.md deleted file mode 100644 index afc49e0e73..0000000000 --- a/.changeset/flow-runs-step-warnings.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -feat(app-shell): surface step warnings in the Flow Runs panel (#3407) - -The automation engine now attaches advisory `warnings[]` to a step whose write -was legally stripped by the data layer — an `update_record`/`create_record` -targeting a `readonly` / `readonlyWhen` field. The step still reports -`success` (the strip is legitimate semantics), so the run trace previously -looked like a clean 3ms success while the intended write never landed; the -only signal lived in the server WARN log. - -`FlowRunsPanel` now reads `step.warnings` and renders each one amber beneath -its step — with a ⚠ marker on the step row — **without** recoloring the -status. The dropped-write signal that #3407/#3413 plumbed from the data layer -into the run's step log now reaches the Studio, closing the observability loop -the author actually looks at. diff --git a/.changeset/form-invalid-scroll-to-error.md b/.changeset/form-invalid-scroll-to-error.md deleted file mode 100644 index fa8c271c2a..0000000000 --- a/.changeset/form-invalid-scroll-to-error.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@object-ui/components": patch ---- - -fix(form): scroll and focus the first errored field on an invalid submit (#2793) - -Submitting a form with a missing required field already toasts the offending field names (#2329), but in a long form the field itself stays off-screen, so the user still hunts for it. react-hook-form's native focus-on-error only reaches fields whose registered ref is a focusable native input — it silently no-ops for custom widgets (lookup / select / master-detail), which is exactly the reported case. The form renderer now disables RHF's unreliable `shouldFocusError` and, in its `onInvalid` handler, scrolls the first errored field (in visual/declared order) into view via its `data-field` wrapper and focuses a focusable control inside it — working for every field type. diff --git a/.changeset/formview-buttons-defaults-fold.md b/.changeset/formview-buttons-defaults-fold.md deleted file mode 100644 index 3837809b88..0000000000 --- a/.changeset/formview-buttons-defaults-fold.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@object-ui/plugin-form": patch -"@object-ui/plugin-view": patch -"@object-ui/app-shell": patch -"@object-ui/types": patch ---- - -feat(form): consume spec-aligned FormView buttons/defaults in ObjectForm - -The authored `@objectstack/spec` FormViewSchema carries structured -`buttons.{submit,cancel,reset}.{show,label}` and `defaults`, but the form -renderer only read the flat renderer-invented `showSubmit`/`submitText`/ -`showCancel`/`cancelText`/`showReset`/`initialValues`. That left the two spec -keys parsed-but-inert (ADR-0078) and stuck at `experimental` in the spec -liveness ledger. - -`ObjectForm` now folds the structured shape down onto those flat props inside -its existing normalization pass, so every entry path (ObjectView -drawer/modal/page, RecordFormPage) honors it. An explicitly-set flat key still -wins, so metadata authored against the deprecated flat keys is unchanged. -`ObjectView` and `RecordFormPage` forward `buttons`/`defaults` from the spec -form view. `ObjectFormSchema` gains the optional `buttons`/`defaults` fields. - -Refs objectstack-ai/objectstack#1894, objectstack-ai/objectstack#2998. diff --git a/.changeset/high-frequency-locale-backfill.md b/.changeset/high-frequency-locale-backfill.md deleted file mode 100644 index 30ef5b5c71..0000000000 --- a/.changeset/high-frequency-locale-backfill.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -feat(i18n): translate the four highest-traffic namespaces into the eight trailing locales (objectui#2872 part a) - -Backfills `console`, `home`, `topbar` and `layout` — 193 keys × 8 packs, 1,544 -strings — so a ja/ko/de/fr/es/pt/ru/ar admin sees the AI console, the home -screen, the top bar and the system navigation in their own language instead of -silently falling back to English. - -The gap in those eight packs drops from **469–471 keys to 277–279**. `en` and -`zh` remain at exact parity (2499 : 2499, zero difference in both directions). - -This is the "high-frequency namespaces only" strategy from the objectui#2872 -discussion, not a full backfill: `grid` (101), `gantt` (58), `dashboard` (25) -and the long tail stay on English fallback and remain tracked there. - -**Four keys are deliberately left untranslated**, and that is the load-bearing -part of this change: - -``` -console.ai.planApproveMessage -console.ai.planApproveDefaultsMessage -console.ai.planAnswerMessage -console.ai.changesConfirmMessage -``` - -These are not labels. They are the text a button *transmits to the agent*, and -the cloud confirm gate (`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`) -decides whether that text reads as approval. It recognises Chinese and English -— nothing else. `AiChatPage` therefore selects them by the language of the -CONVERSATION rather than of the UI, and the `t()` call is *expected* to miss in -every non-Chinese pack and fall through to its English `defaultValue`. - -Translating them would be an outright regression: a German user's "Build it" -would start sending German, the gate would stop matching, and the agent would -re-propose instead of building — the button looks inert while nothing visibly -errors. - -objectui#2900 shipped precisely that bug for `changesConfirmMessage`, which had -been added to all ten packs. **This change removes it from the eight**, -restoring the English fallback. A new guard, -`packages/i18n/src/__tests__/outbound-agent-messages.test.ts`, pins the -invariant in both directions: the four keys must be absent from the eight packs -AND present in `en`/`zh`, while every *other* `console.ai` label must be -translated — so the narrow fix can't be over-applied into an excuse for leaving -surrounding labels in English. - -Translations are model-generated and would benefit from native review; the -placeholder set of every string was verified programmatically against the -English source. diff --git a/.changeset/high-frequency-parity-ratchet.md b/.changeset/high-frequency-parity-ratchet.md deleted file mode 100644 index df6e2176a6..0000000000 --- a/.changeset/high-frequency-parity-ratchet.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -test(i18n): ratchet the four backfilled namespaces so they cannot silently erode - -objectui#2903 translated `console`, `home`, `topbar` and `layout` into all ten -packs. Nothing stopped that from decaying: `fallbackLng: 'en'` means dropping a -key from `de` renders English, which reads as "not translated yet" rather than -"we lost this", and the missing-key handler is dev-only so CI never sees it. - -This is objectui#2872's P3 (full parity test) applied **only to the namespaces -that are actually complete**. Full parity would fail today by ~277 keys per -pack with no action attached to it, which is a broken build rather than a -guard. Widen `RATCHETED_NAMESPACES` as each remaining namespace is translated — -not before. - -Asserts both directions, because the packs have drifted both ways before: - -- every ratcheted `en` key exists in all nine other packs; -- no pack defines a ratcheted key that `en` lacks — objectui#2872 part (b) was - exactly this failure, 74 keys deep, hidden behind a component-private - fallback so English "happened to" render. - -The four outbound agent messages are excluded, since they are deliberately -absent from the eight non-gate packs; `outbound-agent-messages.test.ts` owns -that invariant and the two guards would otherwise contradict each other. - -A non-vacuity assertion pins the ratchet at >300 keys and requires every named -namespace to contribute, so a rename can't quietly reduce the whole file to a -no-op. diff --git a/.changeset/image-field-display-zoom.md b/.changeset/image-field-display-zoom.md deleted file mode 100644 index f650f4ef27..0000000000 --- a/.changeset/image-field-display-zoom.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/fields": patch -"@object-ui/plugin-detail": patch -"@object-ui/i18n": patch ---- - -fix(fields): render `image` fields consistently and add click-to-zoom (#2836) - -An `image` field rendered differently — and wrongly — on three surfaces: - -- **Edit form showed broken thumbnails.** A record read back its `image` value - as a bare `sys_file` id (the reference form), but `readFileValue` returned an - id with no URL — the comment assumed the read path expands it, which the - edit-form data path does not. The result was ``. `file-value` now - derives the stable download URL (`/api/v1/storage/files/:id`, which - 302-redirects to a signed URL and works directly as ``) for a bare - id or an id-only object, so every widget and cell renderer resolves one. -- **Inline edit leaked the raw storage URL.** `InlineFieldInput` had no branch - for file-backed types and fell through to a plain text input showing - `/api/v1/storage/files/…`. It now renders the same upload widgets the form - uses (`image`/`avatar`/`signature`/`file`/`video`/`audio`). -- **Hard-coded English.** `ImageField`'s upload/crop/remove/alt strings now go - through `t('fields.image.*')` (en + zh added). - -Also adds an `ImageLightbox` — click a read-only thumbnail (detail or list cell) -to open a full-screen preview; multiple images get prev/next navigation, a -position counter and arrow-key support, a single image just the image. In a -grid cell the click is `stopPropagation`-guarded so enlarging doesn't also open -the row. diff --git a/.changeset/import-email-client-validation.md b/.changeset/import-email-client-validation.md deleted file mode 100644 index b6ee3dc2ce..0000000000 --- a/.changeset/import-email-client-validation.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -fix(grid): validate email format in the import preview (objectstack#3566) - -The ImportWizard's per-cell `validateValue` did no format check for `email` -columns (it fell through to `default → true`), so an obviously-bad address — -e.g. a non-ASCII domain like `x@柴仟.com` — passed client validation (and the -server dry-run) and only failed at real-import time inside better-auth, giving -a jarring "passed validation, then failed" experience. - -- Added `isPlausibleEmail`, a single-pass structural + ASCII check that mirrors - the server's `isLikelyEmail`, so bad emails are flagged red in the preview - step before submit. No regex backtracking (same ReDoS-safety as the server). diff --git a/.changeset/import-error-localization.md b/.changeset/import-error-localization.md deleted file mode 100644 index e85c86e4dd..0000000000 --- a/.changeset/import-error-localization.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@object-ui/plugin-grid": patch -"@object-ui/i18n": patch ---- - -fix(grid): localize import result errors (objectstack#3566) - -The import completion screen rendered the raw English server message verbatim — -e.g. `Row 6 (position): position: "装配工" matches more than one -os_tianshun_ehr_position — use a unique value or the record id` — with the field -name twice, an internal object api-name, all in English, while the dry-run panel -already localized the same errors. - -- The result list now runs through the same `formatDryRunError` path (driving - off the structured error `code`, resolving the api-name to its field label, - dropping the duplicated `:` prefix). Threaded the error `code` - through `ImportResult.errors` to make this possible. -- Added code-driven translations for the remaining structured import errors — - `invalid_boolean` / `invalid_number` / `invalid_date` / `invalid_option` / - `required` / `AMBIGUOUS_MATCH` — with Chinese (`zh`) copy in `@object-ui/i18n` - alongside the existing reference errors. diff --git a/.changeset/import-historical-checkbox.md b/.changeset/import-historical-checkbox.md deleted file mode 100644 index a46cb82c99..0000000000 --- a/.changeset/import-historical-checkbox.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/plugin-grid": patch -"@object-ui/types": patch ---- - -feat(plugin-grid): "Import as historical data" option in the Import Wizard (framework #3479) - -Adds a checkbox to the Import Wizard's options panel that sends `treatAsHistorical` -on the import request. When on, the server skips the object's `state_machine` rule so -mid-lifecycle rows — a batch of already-`closed` tickets, `closed_won` deals — aren't -rejected by `initialStates`. Off by default: a normal import still walks the FSM, so -the exemption is always an explicit opt-in. - -Pairs with the framework side (objectstack #3483). `ImportRequestOptions.treatAsHistorical` -is added to `@object-ui/types`, and `assembleImportRequest` threads it through both the -inline and named-mapping request shapes (sent only when on). diff --git a/.changeset/import-historical-hint.md b/.changeset/import-historical-hint.md deleted file mode 100644 index 2cab0169c7..0000000000 --- a/.changeset/import-historical-hint.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -docs(plugin-grid): the "Import as historical data" wizard hint now reflects audit-timeline preservation (#3493) - -`treatAsHistorical` gained a second half in framework #3493/#3497 — the import -write context also carries `preserveAudit`, so a historical import keeps the -original `updated_at`/`updated_by` and business `readonly` fields instead of -stamping-now / stripping them. The checkbox hint only described the -state-machine-skip half; it now also says the original timestamps & author are -preserved. The `ImportRequest.treatAsHistorical` type doc (`@object-ui/types`) -is updated to match. Copy-only — no behavior change (the checkbox already sent -`treatAsHistorical`, so the server-side extension is reached without any wiring -change). diff --git a/.changeset/kanban-uncolumned-fallback.md b/.changeset/kanban-uncolumned-fallback.md deleted file mode 100644 index 11f8ee2536..0000000000 --- a/.changeset/kanban-uncolumned-fallback.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@object-ui/plugin-kanban": patch -"@object-ui/i18n": patch ---- - -fix(kanban): surface off-column records in an "Uncategorized" lane instead of dropping them (#2792) - -Records whose `groupBy` value matched no declared column were bucketed and then silently discarded — the board rendered empty while the list footer still counted the rows, so it read as data loss (a status the board doesn't render, an edited/removed picklist option, imported legacy data, or an empty value all triggered it). They now land in a trailing "Uncategorized" lane so no record is invisible and the visible card total reconciles with the record count. Dragging a card out of that lane into a real column repairs its status; the drag handler refuses to persist a move *into* the lane (its sentinel id is not a real option). Adds `kanban.uncategorized` to the en/zh bundles. diff --git a/.changeset/lint-console-runner-react-correctness.md b/.changeset/lint-console-runner-react-correctness.md deleted file mode 100644 index 03ec3917ca..0000000000 --- a/.changeset/lint-console-runner-react-correctness.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@object-ui/console": patch -"@object-ui/runner": patch ---- - -fix(console,runner): the approvals inbox renders against one ticking clock, and both packages now run ESLint - -`apps/console` and `packages/runner` had no `lint` script, so `turbo run lint` -skipped them silently and their 17 ESLint **errors** had never been seen -(#2923 declared them as DEBT; this closes the gap). Both now carry -`"lint": "eslint ."` and the `DEBT` list in `scripts/check-lint-coverage.mjs` -is empty — every workspace package is linted. - -What the errors actually were, once read one by one: - -- **8x `react-hooks/purity` — real, and user-visible.** The approvals inbox - read `Date.now()` mid-render for every age tint, "5m ago" label and SLA chip. - Render must be pure: the output depended on when React happened to render, so - it disagreed with itself under StrictMode's double render and then **froze** — - an inbox left open kept saying "just now" and an SLA countdown never counted - down. The page now renders against a single `now` held in state and advanced - once a minute (the finest granularity anything here displays), so render is a - pure function of props+state *and* the figures actually tick. -- Alongside that, `sla_due_at` is now parsed through a guard. A due date the - backend sends in a shape `Date.parse` can't read used to render as - "SLA NaNh left"; it now renders nothing. -- **1x `react-hooks/static-components` — real.** `StatusBadge` was declared - inside `ApprovalsInboxPage`, making it a brand-new component type on every - render, so React unmounted and remounted every status chip in the table each - time the page re-rendered. Hoisted to module scope, with the translated label - passed as a prop. -- **6x `react-hooks/static-components` — false positives** (3 in the console's - settings pages, 3 in the runner's `LayoutRenderer`). All six render the result - of `getIcon`/`getLazyIcon`, which memoises per name in a module-level cache — - the component reference is stable across renders and nothing is created during - render. The rule cannot see through the call, so these carry the same targeted - `eslint-disable-next-line` + justification the repo already uses at a dozen - icon-registry sites, and the resolvers themselves now say so in a comment. - (Verified rather than assumed: typing into a settings field keeps focus and - every character, so no state was ever being reset there.) -- **2 minor.** A dead `token` initializer on the console's auth preflight path - (`no-useless-assignment` — read, not blind-deleted: no intended write was - missing, every path out of the try/catch either assigns or returns), and a - `prefer-const` in the SDUI workbench preview. diff --git a/.changeset/list-row-crud-effective-ops.md b/.changeset/list-row-crud-effective-ops.md deleted file mode 100644 index 3daafa94e9..0000000000 --- a/.changeset/list-row-crud-effective-ops.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@object-ui/plugin-grid": minor -"@object-ui/plugin-list": minor ---- - -feat: gate list row Edit/Delete and bulk delete on the server's effective operation set (#3720) - -The **fourth** surface #3391 left open. The three earlier rounds — the toolbar -(objectui#2823), detail/form (#3546, objectui#2832 + #2876) and related lists -(#3546) — all route through `resolveCrudAffordances`. The main list's **row -CRUD** does not: it has its own resolver (`plugin-grid`'s -`resolveRowCrudAffordances`), so none of those rounds ever reached it. - -Its gate was `operations ?? { update: !!onEdit, delete: !!onDelete }` — and -`ObjectView` wires `onEdit`/`onDelete` unconditionally while view JSON rarely -declares `operations`, so it was effectively always-on. A caller whose effective -set carried neither `update` nor `delete` still got the row kebab's Edit/Delete -**and** the bulk delete, the most destructive affordance on the list. - -- **plugin-grid** `resolveRowCrudAffordances` now takes `managedBy` and - `effectiveApiOperations` and resolves the object verdict through the shared - `resolveCrudAffordances` policy — so the row gate is the SAME decision the - toolbar, record header, form and related lists make. It also returns - `objectCanDelete`, the object-level delete verdict that bulk delete gates on - (bulk rides `onBulkDelete`, a different callback from the row `onDelete`). -- **plugin-grid** `ObjectGrid` threads its existing `effectiveApiOps` — until - now fed only to Export — into the row gate, and applies the delete verdict to - bulk delete: the implicit `['delete']`, an author-declared - `bulkActions: ['delete']`, and any `bulkActionDefs` entry with - `operation: 'delete'`. A declared bulk action is a *wiring* declaration, not a - permission grant. Custom action ids and non-delete operations pass through - untouched. -- **plugin-list** `ListView`'s own bulk bar (the non-grid views — kanban / - calendar / gallery; the grid path delegates to `ObjectGrid`) drops its - built-in `delete` under the same verdict. - -Also closes the ADR-0103 gap on this chain: `rowCrudAffordances` documented the -bucket lock as "applied upstream via the view's `operations.*`", but the -all-open default meant it never was — an engine-owned `system` / `append-only` / -`better-auth` object leaked a generic row Edit/Delete that the engine rejects -(`assertEngineOwnedWriteAllowed`). Running the shared policy applies it, and a -`userActions` opt-in still re-opens it (e.g. `sys_user`'s `edit`). - -Same semantics as the earlier rounds: **intersection, never union** — a server -grant cannot re-open what the bucket or `userActions` closed, and a -`userActions` opt-in cannot survive a server denial. A missing effective set -(unrestricted object, older backend, or no `PermissionProvider`) preserves the -current behavior. diff --git a/.changeset/listview-columns-vocabulary.md b/.changeset/listview-columns-vocabulary.md deleted file mode 100644 index c62050dc03..0000000000 --- a/.changeset/listview-columns-vocabulary.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/plugin-list": minor -"@object-ui/plugin-view": minor -"@object-ui/app-shell": patch -"@object-ui/types": patch ---- - -refactor(views): ListView reads the spec-canonical `columns`, with legacy `fields` folded in one normalizer (#2890 scope A step 1) - -`ListViewSchema` has been derived from `@objectstack/spec/ui` since #2231, but -the renderer still spoke objectui's own vocabulary for the same concepts. First -rename closed: **`fields` → `columns`**. - -Legacy acceptance does not disappear — stored view metadata in user databases -carries `fields` — but it now lives in exactly one place instead of being -re-implemented per read-site: - -- **New `normalizeListViewSchema` (`@object-ui/core`)** folds `fields` into - `columns` (canonical wins when both are present) and drops the legacy key, so - a read-site that was missed fails loudly instead of quietly taking the legacy - path. It also absorbs the `viewType` renderability default ListView applied - inline. Non-mutating, idempotent, and returns its input by reference when - there is nothing to fold, so ListView's downstream memos keep a stable - dependency identity. -- **`ListView` normalizes once at the component boundary**, before anything - reads the schema. This is what guarantees the fold runs: nothing on the render - path parses view metadata through zod (the zod schemas serve the CLI - validator, the VS Code extension and tests), so a `z.preprocess` on - `ListViewSchema` — spec-side or local — would never execute. -- **Producers emit `columns`**: `ObjectView`'s `renderListView` payload, - `ObjectDataPage`, `InterfaceListPage` and the `list-view` registry defaults - had been *downgrading* already-canonical `columns` config back to `fields`. - -Two latent inconsistencies go away with it: the filter builder's -objectDef-not-loaded fallback now resolves `ListColumn.field` (it read only -`name`/`fieldName`, so object-form columns produced unnamed filter entries), and -the column list no longer depends on which of the two keys a host happened to -emit. - -`fields` stays declared on `ListViewSchema` and in the drift guard's sanctioned -set — it is still valid input, and `@objectstack/spec`'s `react-blocks.ts` -sanctions it as the React-tier `` prop — but it is input-only. diff --git a/.changeset/listview-filter-vocabulary.md b/.changeset/listview-filter-vocabulary.md deleted file mode 100644 index d5abcdfd32..0000000000 --- a/.changeset/listview-filter-vocabulary.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/plugin-list": minor -"@object-ui/plugin-view": minor -"@object-ui/app-shell": minor -"@object-ui/types": patch ---- - -fix(views): ListView reads the spec-canonical `filter`, so a view's base filter reaches every visualization (#2890 scope A step 4) - -Third rename in the ListView vocabulary migration: **`filters` → `filter`**. Unlike -the first two this closes a live bug, because the fork was asymmetric. - -`ListView` was the **only** surface in the repo reading `filters`. Every child -view — `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`, -`ObjectGantt`, `ObjectMap`, `ObjectTree`, `ObjectChart` — reads `filter`, and -`ListView` handed them `filters`. Wherever a child fetches its own rows instead -of receiving `ListView`'s, the view's base filter was silently dropped: - -- **a `chart` list view aggregated the whole object.** The chart branch built an - `object-chart` node with `filters:`; `ObjectChart` reads `schema.filter` and - never read `filters`, so a chart view with a base filter charted unfiltered - totals. -- the same applied to any of the other view components rendered standalone from - a list-view-shaped config. - -Conversely, a **spec-authored** list view — one carrying `filter`, which is what -the spec says and what `runtime-metadata-persistence` and "Save as view" already -persist — rendered **unfiltered** in `ListView`, because nothing read that key. - -The fold is a key rename only. Both keys carry an ObjectQL FilterNode array -everywhere in objectui; every consumer passes the value straight to `$filter`. -(The spec types `filter` as `ViewFilterRule[]` — `{field, operator, value}` -objects — so objectui's field is typed from the spec but used as something else. -That mismatch is real and left alone here: converting formats inside a -vocabulary fold would change what reaches the data source.) - -Also collapses a duplicated computation in `app-shell`'s `ObjectView`, which -computed the same effective filter **twice** — once as `filter` for the child -views, once as `filters` for `ListView` — with the two copies subtly different -(only one fell back to `listSchema.filter`; only the other ran token -substitution over the URL filters). There is now one computation, keeping both -behaviors. - -`filters` stays declared on `ListViewSchema` and in the drift guard's sanctioned -set — stored views carry it and it is still valid input — but it is input-only. diff --git a/.changeset/listview-record-count-total.md b/.changeset/listview-record-count-total.md deleted file mode 100644 index 74de83d760..0000000000 --- a/.changeset/listview-record-count-total.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/plugin-list": patch ---- - -fix(list): show the real match total in the record-count status bar under server pagination - -The Airtable-style record-count bar read `data.length`, but under server-side -pagination (#2212) `data` is only the current page window — so a 158-row result -paginated 100/page reported "100 条记录" on page 1 and "58 条记录" on page 2, -never the true total. There was no other place to see how many records the -query matched. - -The bar now shows the server's grand total (`serverTotal`) when known, falling -back to `data.length` when the whole result set is in memory (non-paginated, -grouped and non-grid views are unchanged — `serverTotal` is null there, so the -count is identical to before). Browser-verified against the showcase contacts -list: the bar reads "158 条记录" and stays stable across pages, and switching to -grouped/other views correctly resets to the loaded count. diff --git a/.changeset/listview-rowheight-vocabulary.md b/.changeset/listview-rowheight-vocabulary.md deleted file mode 100644 index 380182b64f..0000000000 --- a/.changeset/listview-rowheight-vocabulary.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@object-ui/core": minor -"@object-ui/plugin-list": minor -"@object-ui/app-shell": minor -"@object-ui/types": patch ---- - -refactor(views): ListView resolves density from the spec-canonical `rowHeight` (#2890 scope A step 2) - -Second rename in the ListView vocabulary migration: **`densityMode` → `rowHeight`**, -folded in the same `normalizeListViewSchema` that step 1 introduced. - -Unlike `fields`/`columns` this is not a pure alias — the two vocabularies are -different sizes. The spec has five row heights (`compact`/`short`/`medium`/ -`tall`/`extra_tall`); ListView's toolbar offers three densities -(`compact`/`comfortable`/`spacious`). Both directions now live in one place as -`DENSITY_MODE_TO_ROW_HEIGHT` / `ROW_HEIGHT_TO_DENSITY_MODE`, chosen so a fold -followed by a read is a round trip (`spacious` → `tall` → `spacious`), with the -narrowing collapse (`short` → `compact`, `extra_tall` → `spacious`) stated once -instead of being re-derived per call site. - -Two behavior fixes fall out of it: - -- **Precedence is no longer inverted.** `ListView` read `densityMode` *first*, so - a view carrying both keys rendered the legacy value — backwards from every - other legacy/canonical pair in the schema. The canonical key now wins. -- **The toolbar stops re-seeding the legacy key.** `ObjectView`'s - `onDensityChange` persisted `densityMode` into stored view metadata on every - density toggle, so the legacy vocabulary kept regrowing underneath the - migration. It persists `rowHeight` now. - -`densityMode` stays declared on `ListViewSchema` and in the drift guard's -sanctioned set — stored views carry it and it is still valid input — but it is -input-only. diff --git a/.changeset/listview-sort-blank-repeater.md b/.changeset/listview-sort-blank-repeater.md deleted file mode 100644 index 95da12b2be..0000000000 --- a/.changeset/listview-sort-blank-repeater.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(SchemaForm): render row sub-fields for `repeater` fields whose schema is a union (objectui#3379) - -In Edit View Config → Columns & Filters → Sort, "Add" produced an empty row -with no field picker or order dropdown. A View's `sort` prop is a -`z.union([z.string(), z.array(z.object({ field, order }))])`, so its JSONSchema -is `anyOf: [string, {field,order}[]]`. The SchemaForm repeater read -`schema.items` at the top level — which is `undefined` for a union — and -derived zero sub-fields. - -The repeater now resolves the union to its array branch and uses that branch's -`items` for both the derived field list and the per-row controls -(`pickSubSchema`). The legacy bare-string `sort` form remains valid in the spec -(its removal is a separate, deferred deprecation cycle); this is purely a -renderer fix. diff --git a/.changeset/locale-key-parity-en-zh.md b/.changeset/locale-key-parity-en-zh.md deleted file mode 100644 index d8b508b6c9..0000000000 --- a/.changeset/locale-key-parity-en-zh.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/plugin-grid": patch -"@object-ui/console": patch ---- - -fix(i18n): make `en` the complete source of truth for grid import and set-password (objectui#2872 b/c) - -The `en` and `zh` packs had drifted in both directions, silently, because -`fallbackLng: 'en'` degrades a missing key into English rather than an error and -the missing-key handler only fires in dev. - -- **74 keys existed only in `zh`.** `grid.import.*` and `auth.setPassword.*` had - never been added to `en`, so no other locale could translate them: the English - text came from call-site `defaultValue:` args and a private map inside - `ImportWizard`. They now live in `en`, which is what translators and - `os i18n extract` read. -- **4 `en` keys were missing from `zh`** (`console.commandPalette.title`, - two `console.ai.suggestions.metadataAssistant.*`, `help.keyboardShortcuts`), - so Chinese users saw English. - -`grid.import` in particular had three disagreeing sources — the `en` pack (62 -keys), `zh` (130) and `ImportWizard`'s own fallback map (133), union 134, no two -the same set. All three are now aligned on 134. - -The wizard's fallback map is kept, not deleted: it is what lets the wizard render -with no `I18nProvider` mounted (standalone embedding, unit tests). It is instead -pinned to the `en` pack by a new test, so the two can no longer drift. - -`SetPasswordPage` drops its now-redundant inline `defaultValue:` args; the text -is byte-identical, it just comes from the pack now. - -Adds two guards, both mutation-verified: -- `en` ↔ `zh` full key parity, asserted in both directions. The other eight - packs are still ~357 keys behind and are tracked separately (objectui#2872 - part a), so they are deliberately not asserted yet. -- `IMPORT_DEFAULT_TRANSLATIONS` ↔ `en.grid.import`, same keys and same text. diff --git a/.changeset/modal-action-target-page-resolution.md b/.changeset/modal-action-target-page-resolution.md deleted file mode 100644 index 0799932e71..0000000000 --- a/.changeset/modal-action-target-page-resolution.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(console): resolve a modal action's `target` as a page, not an object (#3530) - -Submitting a `type: 'modal'` action failed with "Error loading form — Bad -Request". The console read the action's `target` as an OBJECT name and opened a -create form for it, so a target naming a page issued `GET -/meta/object/` — which 400s — and the dialog rendered ``'s -error state instead of the page. Every modal action in an app hit this; the only -workaround was re-authoring each one as a screen flow. - -The spec is explicit that for `type: 'modal'`, `target` is "the modal/page name -to open". - -- `normalizeModalSchema` no longer guesses "object" for a string target. It - records the raw name and `useActionModal.resolveModalTarget` (new) resolves it - against metadata: **page first**, then object for back-compat. Resolution uses - `getItem(type, name)`, a single-item fetch, so it never eagerly loads the lazy - page/object lists — this hook is mounted at the console root. -- The `create_x` / `edit_x` prefix convention still yields an object form, but - now only as a fallback: a page actually named `create_opportunity` wins over - the object `opportunity` the name would otherwise be parsed into. -- A target that names neither reports what is wrong ("Modal target "x" matches - no page or object") instead of surfacing a downstream HTTP error. - -Modal dispatch is also now the same on every console surface. `type: 'modal'` -was wired straight to the server-action POST in `useConsoleActionRuntime` (list -pages, SDUI pages, the declared-actions bar) while `RecordDetailView` opened -modals client-side — the same button did two different things depending on where -it was mounted. Both now run one rule: render `target` when it names a page or -object, otherwise complete the action through its server-side handler, so a -modal action bound to `engine.registerAction(...)` keeps working. diff --git a/.changeset/naming-drift-dashboard-label-field-length.md b/.changeset/naming-drift-dashboard-label-field-length.md deleted file mode 100644 index 6edbad34d1..0000000000 --- a/.changeset/naming-drift-dashboard-label-field-length.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/plugin-dashboard": patch -"@object-ui/fields": patch ---- - -fix: read spec-canonical keys for dashboard header title and field length rules - -Two naming-drift closeouts (framework#1878 / framework#1891): - -- `DashboardRenderer` header now falls back to the spec-canonical `label` when - the legacy `title` is absent (mirrors the `DashboardGridLayout` fallback from - #2666) — a spec-compliant dashboard gets its header title. -- Field validation rules now read the spec-canonical camelCase - `minLength`/`maxLength` (what the server record-validator enforces) with the - legacy snake_case `min_length`/`max_length` kept as fallback — authored - length constraints reach the client form. diff --git a/.changeset/naming-drift-page-and-maxlength.md b/.changeset/naming-drift-page-and-maxlength.md deleted file mode 100644 index 4dbf4218fd..0000000000 --- a/.changeset/naming-drift-page-and-maxlength.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@object-ui/app-shell": patch -"@object-ui/components": patch -"@object-ui/plugin-form": patch -"@object-ui/fields": patch ---- - -fix(page,field): consume the spec's `type`/`label`/`maxLength` keys (framework#1878 §3 naming-drift recheck) - -Three forward-drifts where objectui read a different key than the spec -declares, so authoring the documented key silently no-oped: - -- **page `type` → `pageType`** (app-shell + components): `PageSchema` declares - the page KIND as `type`, but `PageRenderer` reads `schema.pageType` and fell - back to `'record'` — and nothing mapped between them. Every non-record page - (`home`/`app`/`list`/`utility`) rendered with the record max-width, a wrong - `data-page-type` attribute, and a suppressed header. `PageView` now passes - `pageType` alongside the SchemaNode discriminator `type`. -- **page `label` → `title`** (components): `PageSchema.label` is required but the - region renderer read only `title`. Now dual-reads `title ?? label`, mirroring - the fallback `DashboardRenderer` already uses. Coupled with the above — the - header is gated on `pageType !== 'record'`, so both were needed for a title to - appear. -- **field `maxLength`/`minLength`** (plugin-form + fields): validation already - dual-read these, but `ObjectForm`'s HTML-attribute pass and `TextAreaField` - read `max_length` only, so a spec-authored `maxLength` gave no browser cap and - no character counter. Both now dual-read, matching `buildValidationRules`. - -Verified in the browser against the showcase: `capability_map` (`type: 'home'`) -now renders `data-page-type="home"`, the `home` max-width and its page title; -record pages are unchanged. diff --git a/.changeset/no-try-catch-around-hook.md b/.changeset/no-try-catch-around-hook.md deleted file mode 100644 index a77678c31d..0000000000 --- a/.changeset/no-try-catch-around-hook.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/plugin-detail": patch -"@object-ui/plugin-timeline": patch -"@object-ui/plugin-list": patch -"@object-ui/plugin-calendar": patch -"@object-ui/plugin-grid": patch -"@object-ui/plugin-designer": patch -"@object-ui/plugin-gantt": patch -"@object-ui/plugin-view": patch -"@object-ui/components": patch ---- - -fix(hooks): stop calling translation hooks inside try/catch (objectui#2879) - -Eleven call sites wrapped a React hook in `try`/`catch` to make it -"provider-safe". `useObjectTranslation` and `useObjectLabel` already are — they -read context optionally and fall back to react-i18next's global instance, and -never throw. The `catch` bought nothing and cost correctness: a throw *after* -the hook ran desyncs hook order on the next render, because React matches hooks -positionally. objectui#2595/#2596 fixed exactly this in `@object-ui/i18n`'s -`createSafeTranslation`; nine plugin-local re-implementations kept their own -copy of the bug, and two more (`ObjectTimeline`, `ObjectView`) were found by the -new lint rule below — `ObjectView` had even suppressed -`react-hooks/rules-of-hooks` inline to keep it. - -- Six exact re-implementations now delegate to `createSafeTranslation`: - `plugin-detail`, `plugin-timeline`, `plugin-list`, `plugin-calendar`, - `plugin-grid`'s `ObjectGrid`, `plugin-designer`. -- `components`' `data-table` also delegates; `createSafeTranslation` now - returns `language` alongside `t` so consumers that localize dates don't need - a second hook call. Purely additive. -- `plugin-gantt` and `plugin-grid`'s `ImportWizard` keep their local hooks — - they fall back *per key*, which a single-probe factory cannot express and - which their comments justify (a host dictionary that covers common keys but - lags on newer ones). Only the `try`/`catch` is removed. -- `ObjectTimeline` and `ObjectView` call the hook directly and probe the - returned value, mirroring `useSafeFieldLabel`. - -Adds `object-ui/no-try-catch-around-hook` (error) so a twelfth copy fails CI. -It only matches `use*` names, accepts member calls solely on `React` (so -`vi.useRealTimers()` is not a hook), and resets its try-depth inside nested -functions (so `renderHook(() => useThing())` inside a `try` is fine) — both -false positives were real code in this repo and are pinned in the rule's tests. - -`eslint-rules/**/*.test.js` matched no vitest project glob, so the local -plugin's specs had never run in CI. They are now included; all three pass. - -`ObjectTimeline`'s test mock of `@object-ui/react` omitted `useObjectLabel` — -the removed `try`/`catch` had been silently absorbing that gap. The mock is now -complete. diff --git a/.changeset/objectchart-fieldless-count-column.md b/.changeset/objectchart-fieldless-count-column.md deleted file mode 100644 index 8571f0c655..0000000000 --- a/.changeset/objectchart-fieldless-count-column.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@object-ui/plugin-charts": patch -"@object-ui/data-objectstack": patch ---- - -fix(charts): a fieldless `count` aggregate keyed its value column `undefined`, so the chart plotted nothing (framework#3701) - -framework#3701 pinned down what an OBJECT-bound chart aggregate names its result -columns — the raw field names it was given (`groupBy` for the category, `field` -for the value; no `sum_`-style decoration, unlike a dataset measure), plus the -literal `count` when a `count` omits `field`, which is the alias the engine -projects `COUNT(*)` under. `os validate` now lints page sources against that -convention, so the paths that build these rows have to honour it exactly. - -Three of the four did. The odd one out was `count` — the one function that may -legitimately omit `field` — because every row builder read `params.field` -directly: - -- `aggregateRecords` / `ObjectDataSource.aggregateClientSide` emitted - `{ [groupBy]: key, [undefined]: value }`, i.e. a column literally named - `undefined` that no axis binding could ever name; -- the legacy analytics path was worse: it remapped the server's `count` measure - onto `params.field` and **deleted** the original key, so the value the server - did return was thrown away before the chart saw it. - -All of them now resolve the column through one helper (`aggregateValueKey`) so a -fieldless count lands under `count`, matching the framework contract. The -comparison-overlay column is derived from the same key (`count__comparison` -instead of `undefined__comparison`), and `aggregate.field` is typed optional to -match the spec's `ChartAggregateSchema`. Charts that name a field are unchanged. diff --git a/.changeset/objectchart-honors-chartconfig.md b/.changeset/objectchart-honors-chartconfig.md deleted file mode 100644 index 1132b19c98..0000000000 --- a/.changeset/objectchart-honors-chartconfig.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@object-ui/plugin-charts": minor -"@object-ui/components": patch ---- - -feat(charts): ObjectChart honors the spec `ChartConfig` author shape (objectui#2880 / framework#3729) - -`ChartConfigSchema` is the chart protocol, but the renderer only ever read a -Recharts-flavoured internal shape — `chartType`, `xAxisKey`, `series[].dataKey`. -Everything an author wrote in the SPEC shape reached the renderer and was -silently dropped, which is exactly what ADR-0078 forbids. framework#3725 -documented the gap by trimming the published contract down to the props that -actually worked; this closes it the other way round. - -**S1 — one normalization boundary.** `normalizeChartSchema` translates the -author shape into the internal pipeline contract in a single place, rather than -scattering `??` fallbacks through the render tree (framework PD #12: one -translation is a contract mapping, N fallbacks are a second dialect): - -- `type` → `chartType`, `xAxis: { field }` → `xAxisKey`, `series: [{ name }]` → - `series: [{ dataKey }]` -- the report surface's bare-string `xAxis`/`yAxis` resolve too -- `yAxis: [{ field }]` alone plots, with no `series` declared -- **internal props win**, so `DashboardRenderer`, `ObjectView` and the dataset - path are byte-for-byte unaffected — there is no migration - -**The `type` collision.** `ChartConfig.type` is the chart family, but on any -surface that flattens chart config into a props bag `type` is already the SDUI -envelope's component discriminator. Spreading props last let an author's -`type="bar"` replace `object-chart` so the block stopped resolving; stamping the -discriminator last ate the author's value instead. The react-page wrapper now -keeps both: the discriminator wins the `type` slot and the author's value is -preserved beside it as `specType`, which the normalizer reads back. - -**S2 — axis presentation.** `ChartAxis.format` drives the tick formatter (via -`Intl.NumberFormat`, no new dependency), `min`/`max` pin the domain, -`logarithmic` swaps the scale, `title` labels the axis, and `showGridLines` is -honored. A second `yAxis` entry (or `position: 'right'`) turns on the secondary -axis that `series[].yAxis` binds to — in combo charts an explicit binding now -beats the family-derived bar→left/line→right guess. `showLegend` is honored, -and `title`/`subtitle` render above the plot instead of only titling the -drill-down drawer. - -**S3 — `series[].stack`, `annotations`, `interaction`.** Stacking passes the -author's group name through as Recharts' `stackId`. Annotations render as -`ReferenceLine` (`type: 'line'`) / `ReferenceArea` (`type: 'region'`) with the -declared axis, colour, style and label. `interaction.tooltips: false` suppresses -the hover card and `interaction.brush: true` adds the range selector; -`showDataLabels` prints values on the marks. `interaction.zoom` has no Recharts -primitive behind it and is deliberately still unimplemented rather than faked. diff --git a/.changeset/org-role-vocabulary-closed.md b/.changeset/org-role-vocabulary-closed.md deleted file mode 100644 index 325ea772ef..0000000000 --- a/.changeset/org-role-vocabulary-closed.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -docs(auth): the org-role vocabulary is closed — correct the mirror's standing instruction (framework ADR-0108) - -`org-roles.ts` carried a standing instruction that is now wrong: *"a role added -server-side must be added HERE too."* There are no server-side additions left -to chase. - -The framework used to register every declared `position` / `permission` name as -an organization role, so the console's list could always fall behind the -server's. That channel was retired (framework ADR-0108, objectstack#3723): -every value stored in `sys_member.role` is projected into -`current_user.positions`, so a business role handed out that way was capability -with none of the position system's controls — no `granted_by`, no validity -window, no scope check. `sys_member.role` is now a closed, framework-owned list -of `owner` / `admin` / `delegated_admin` / `member`, and an app's own business -roles are positions, granted through `sys_user_position` or an invitation's -placement (framework ADR-0105 D8). - -So this mirror is now complete **by construction** rather than by vigilance. -Nothing about the console's behaviour changes — the four names and their labels -are what they already were. - -Still a mirror rather than a derivation, but only for a packaging reason now: -the names live in `@objectstack/spec` as `BUILTIN_MEMBERSHIP_ROLES` / -`BUILTIN_MEMBERSHIP_ROLE_OPTIONS`, which `@object-ui/auth` cannot import yet — -this package takes no dependency on `@objectstack/spec`, and those constants -ship in the first release carrying ADR-0108 (they are absent from the published -16.1.0). A new test pins the list to exactly those four in display order until -then, so drift fails loudly instead of silently offering a value the server's -enforced `select` would reject. diff --git a/.changeset/record-detail-global-action-labels.md b/.changeset/record-detail-global-action-labels.md deleted file mode 100644 index fd02d9b3e9..0000000000 --- a/.changeset/record-detail-global-action-labels.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/i18n": patch ---- - -fix(i18n): apply globalActions label overlays to actions surfaced on a record-detail action bar (objectui#3372) - -On a record-detail action bar the caller passes `objectDef.name` for **every** -action, so a `globalAction` surfaced there (e.g. `log_call`) looked up -`objects.._actions..label`, missed, and leaked the English -metadata literal ("Log a Call") instead of its `globalActions..label` -overlay ("记录通话"). Object-owned actions on the same bar translated fine, -which is what made the gap visible. - -`useObjectLabel()`'s action resolvers now mirror the canonical -`@objectstack/spec` resolver (`system/i18n-resolver.lookupActionField`): when an -action is object-scoped, the object key still wins, but `globalActions..*` -is consulted as a fallback before returning the literal. This applies uniformly -to `actionLabel`, `actionConfirm`, `actionSuccess`, `actionDescription`, -`actionResultDialog`, `actionParamText`, and `actionParamOptionLabel`, so a -globalAction resolves the same on a record-detail action bar as it does -everywhere else. App-namespace discovery also recognises a `globalActions`-only -bundle (one with no object/field entries). diff --git a/.changeset/remediation-overlay-i18n.md b/.changeset/remediation-overlay-i18n.md deleted file mode 100644 index 1b5958c05e..0000000000 --- a/.changeset/remediation-overlay-i18n.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@object-ui/i18n": patch -"@object-ui/app-shell": patch ---- - -fix(auth): localize the ADR-0069 remediation gate and the auth split-panel (#2870) - -`RemediationOverlay` had no i18n at all. It is the full-screen gate mounted -unconditionally at `ConsoleShell` (`fixed inset-0 z-[200]`) that a user hits -when the backend returns `PASSWORD_EXPIRED` or `MFA_REQUIRED` — there is no -route around it, so a user who could not read English could not get back into -the product. That makes it a usability block rather than a cosmetic gap. - -- New `auth.remediation.*` namespace in all ten locale packs, covering both - branches of the gate: expired-password (title, three field labels, submit / - submitting, mismatch and failure messages) and MFA enrolment (password step, - QR scan copy, backup-code disclosure, code entry, verify / verifying, and the - enrolment and invalid-code failures), plus the shared "sign out instead" exit. -- Validation and failure messages are translated where they are raised, since - they are held in component state and rendered later. -- The server-provided `remediationRequired.message` is left untouched; only the - empty-message fallback is localized. -- `AuthPageLayout`'s two marketing strings move to `auth.layout.*`. The forms it - wraps were already localized, so the split-panel had been rendering half in - the user's language and half in English. - -Adds a locale-parity test over both namespaces, asserting an identical key set -across all ten packs, a non-empty string at every leaf, and that prose differs -from English (short labels like "Continue" legitimately collide). i18next falls -back to `en` silently and its missing-key handler is dev-only, so a key added to -one pack and forgotten elsewhere is invisible in whichever locales get tested by -hand. diff --git a/.changeset/remove-dead-field-permissions-wire-fls.md b/.changeset/remove-dead-field-permissions-wire-fls.md deleted file mode 100644 index 52a99ad8d9..0000000000 --- a/.changeset/remove-dead-field-permissions-wire-fls.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@object-ui/types": major -"@object-ui/app-shell": patch -"@object-ui/plugin-form": patch -"@object-ui/plugin-grid": patch ---- - -fix(fls): wire the real per-caller FLS channel into import targets and grid -columns; remove the never-populated `field.permissions` shape (objectstack#3661) - -The `permissions?: { read?, write?, edit? }` key on `@object-ui/types` field -definitions (Phase 3.2.6) was declared-but-never-enforced: no producer in the -stack ever populated it, so every guard reading it short-circuited to "allow". -Per ADR-0049 enforce-or-remove, the shape is deleted and the three consumers -now use the server-resolved `/auth/me/permissions` channel -(`usePermissions().checkField`) — the same channel ObjectForm/ModalForm/ListView -already enforce: - -- **ImportWizard target fields (app-shell `ObjectView`)**: the importable - field set (and thus the downloadable CSV template's columns) now drops - fields the caller cannot edit, instead of offering columns the server's - FLS write gate would 403. -- **ObjectGrid auto-derived columns**: columns the caller cannot read are - dropped (same gate ListView applies), instead of a dead schema-shape check. -- **ObjectForm**: the redundant dead guard in field generation is removed; - the existing `applyFieldPerms` gate remains the real enforcement point. - -BREAKING CHANGE: `@object-ui/types` field definitions no longer accept a -`permissions` key. It never carried data at runtime; consumers needing -per-caller field-level permissions must use `@object-ui/permissions` -(`MePermissionsProvider` + `useFieldPermissions`/`checkField`). diff --git a/.changeset/remove-record-change-trap.md b/.changeset/remove-record-change-trap.md deleted file mode 100644 index ca6f2b5170..0000000000 --- a/.changeset/remove-record-change-trap.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(app-shell): remove the never-firing `record-change` option from the flow trigger picker (#3427) - -The Studio flow designer's start-node trigger picker offered "Record changed -(any)" (`record-change`), but the runtime routes it to the record-change trigger, -which maps it to no ObjectQL hook — so the flow binds yet **never fires**. Authoring -it produced a silently-dead flow. Removed the option (and dropped `record-change` -from the scope resolver's record/previous sets and the zh-CN labels). The common -"created or updated" case is covered by `record-after-write`; a companion -`@objectstack/lint` rule flags any hand-authored `record-change` at `os validate` -time. diff --git a/.changeset/retire-reportviewer-legacy-chart-fallback.md b/.changeset/retire-reportviewer-legacy-chart-fallback.md deleted file mode 100644 index 04d8a746d9..0000000000 --- a/.changeset/retire-reportviewer-legacy-chart-fallback.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@object-ui/types": minor -"@object-ui/plugin-report": minor -"@object-ui/app-shell": minor ---- - -feat(report)!: drop `SpecReportColumn`/`SpecReportGrouping` re-exports + retire the legacy ReportViewer chart fallback (#3463) - -Cross-repo close-out of the ADR-0021 report cleanup (framework #3463). Upstream -`@objectstack/spec` removed the dead `ReportColumnSchema` / `ReportGroupingSchema` -and the unread report `chart.groupBy`; this drops their objectui mirrors and the -now-orphaned legacy report chart path. - -- **types**: removed the `SpecReportColumn` / `SpecReportColumnInput` / - `SpecReportGrouping` / `SpecReportGroupingInput` type re-exports and the - `SpecReportColumnSchema` / `SpecReportGroupingSchema` value re-exports from - `@object-ui/types` (they aliased the deleted upstream symbols). The live - report shape is dataset-bound — `SpecReport` with `dataset` + `values` - (measure names) + `rows` / `columns` (dimension names). -- **app-shell**: `ReportView` now renders every report through the spec - `ReportRenderer` dispatcher (dataset → `DatasetReportRenderer`, stored pre-9.0 - JSON → presentation bridge, pre-spec `{ data, columns }` → `LegacyReportRenderer`). - Deleted the `ReportViewer` last-resort branch, the `mapReportForViewer` - spec→legacy chart-section adapter (the sole producer of `xAxisField` / - `yAxisFields`), and the now-dead data-fetch loading flag. No shipped report - metadata reached the removed branch — the Studio inspector only ever writes - the dataset-bound shape. -- **plugin-report**: removed the `ReportViewer` chart-section branch. It read - the invented `xAxisField` / `yAxisFields` (never the spec's `xAxis` / `yAxis`) - and was only fed by the deleted `mapReportForViewer`. `ReportViewer` itself is - retained — its table / summary / text sections still back the `report-viewer` - registered component and the pre-9.0 presentation bridge. - -**Migration**: nothing an author writes changes. TypeScript consumers importing -`SpecReportColumn*` / `SpecReportGrouping*` from `@object-ui/types` have no -replacement type — model report columns as the dataset's measure names and -grouping as its dimension names. diff --git a/.changeset/runner-typecheck-2917.md b/.changeset/runner-typecheck-2917.md deleted file mode 100644 index e52c02c641..0000000000 --- a/.changeset/runner-typecheck-2917.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -"@object-ui/runner": patch ---- - -fix(runner): type-check the package at all, and fix the `DataSource` contract violation that hid behind a broken import (#2917) - -`@object-ui/runner` was the worst-covered package in the repo: `build` is -`vite build` (transpile only), it had no `type-check` script, and — uniquely — -**no `tsconfig.json` at all**. Nothing had ever type-checked it, despite it being -a published package. - -**It was not broken at runtime.** The two bad imports were `import type`, so they -were erased before they could fail, and the one value import -(`emulateBatchTransaction`) does exist. `MockDataSource` is also unreferenced -anywhere in the repo. So this is a correctness and reference-quality fix, not an -outage. - -**What the missing check actually hid.** `DataSource` and -`BatchTransactionOperation` were imported from `@object-ui/core`, which does not -export them — they live in `@object-ui/types`. Because that import never -resolved, `class MockDataSource implements DataSource` was silently a no-op, and -three separate commits maintained the class *as if* it were being verified -(`62b9ab510` added `batchTransaction`, `09d9669c7` made `getObjectSchema` -required, `5527388b0` added input validation). With the `implements` clause -inert, a real contract violation survived all three: - -```ts -async find(resource: string, params?: any): Promise { return []; } -``` - -`DataSource.find` returns a `QueryResult` envelope, not a bare array. Anyone -copying this mock as the starting point for their own adapter — which is exactly -what its doc comment invites — would hand every consumer an array where `.data` -and `.total` are `undefined`. Now typed as `Promise` and returning -`{ data: [], total: 0 }`. - -Also in this change: - -- `packages/runner/tsconfig.json` added, mirroring `apps/console` rather than the - library packages: `runner` is a Vite app, so it wants `bundler` resolution, - `allowImportingTsExtensions` (for `./App.tsx`) and `types: ["vite/client"]` - (for `import.meta.glob` in `MetadataLoader` and the `./index.css` side-effect - import). Keeping it standalone instead of extending the root config also means - it never inherits the root `paths`, so workspace deps resolve through built - `.d.ts` and the TS6059 `rootDir` class of error cannot appear. -- unused parameters prefixed with `_` (6x in `mockDataSource`), and an unused - `Circle` icon import dropped from `LayoutRenderer`. -- `"type-check": "tsc --noEmit"` added, and the package's `DEBT` entry deleted - from `scripts/check-type-check-coverage.mjs`. Coverage goes 35 -> 36 of 45 and - outstanding errors 46 -> 32. - -Verified the gate genuinely covers the package now, rather than trusting the -green: injecting a type error into `runner/src/App.tsx` makes `pnpm type-check` -fail with `Failed: @object-ui/runner#type-check`, which was impossible before -this change. diff --git a/.changeset/screen-field-visible-when.md b/.changeset/screen-field-visible-when.md deleted file mode 100644 index 7efb6b96e3..0000000000 --- a/.changeset/screen-field-visible-when.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(flow-runner): honor a screen field's `visibleWhen` — in rendering AND in required-enforcement (framework#3528) - -A paused screen-flow rendered every declared field regardless of its -`visibleWhen` predicate, while still enforcing `required` over the full list. -Where a field is optional-by-design but required *when shown*, that combination -dead-ends the run: Submit blocks on an input the user was never shown, issues -**zero network requests**, and the flow sits paused forever. - -Reproduced in Chromium against a real HotCRM dev server — on both the console -shipped with `@objectstack/*` 16.1.0 and current `main`: - -``` -→ POST /api/v1/automation/lead_conversion/trigger 200 {status: paused, screen} - rendered: ["Create Opportunity? *", "Opportunity Name *", "Opportunity Amount"] - click Submit (checkbox untouched) -→ (nothing) resume calls: 0 toasts: none dialog: still open -``` - -The predicate never reached the client — the framework declared `visibleWhen` on -the screen node's designer form but dropped it when building the paused payload -(fixed in objectstack#3771). This is the consumer half. - -- **`visibleScreenFields(screen, values)`** is the single source of truth for - what is on screen. `ScreenView` renders from it and `FlowRunner.submit()` - validates from it, so the two can never disagree — splitting them is the bug. -- Predicates are **bare CEL over the screen's own field names** - (`createOpportunity == true`), evaluated through the canonical - `@objectstack/formula` engine, the same verdict the server reaches for field - rules. Values bind both bare and under `record.`. -- **Declared fields are seeded before evaluation.** An untouched checkbox holds - `undefined`, which CEL treats as an unknown identifier — the evaluation errors - and falls open, leaving the dependent field on screen in exactly the state - where it should be hidden. Booleans seed `false`, everything else `null`. -- **Fail-open is preserved for genuinely broken predicates** (syntax error, or a - name that is not a field on this screen), matching `resolveFieldRuleState`: - hiding an input on a typo would silently drop data the flow is waiting for. - -Screens with no `visibleWhen` behave exactly as before. diff --git a/.changeset/search-page-record-hits.md b/.changeset/search-page-record-hits.md deleted file mode 100644 index 28f91fc5d1..0000000000 --- a/.changeset/search-page-record-hits.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -The full-page search (`/apps/:app/search`) now surfaces record hits, not just -metadata nav items. - -Following the ⌘K command-palette fix (#3371), the search results page was still -matching only navigation entries (objects, dashboards, pages, reports). It now -runs the same global record search (`useRecordSearch` → `/api/v1/search`), -scoped to the app's searchable objects, and renders the record hits grouped by -object above the metadata matches. Both the search page and the palette now -resolve each object group's heading through the i18n label resolver, so -localized object labels display correctly instead of falling back to the raw -object name. diff --git a/.changeset/sharing-rule-widgets-i18n-and-orderby.md b/.changeset/sharing-rule-widgets-i18n-and-orderby.md deleted file mode 100644 index bc461eedfc..0000000000 --- a/.changeset/sharing-rule-widgets-i18n-and-orderby.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -"@object-ui/core": patch -"@object-ui/types": patch -"@object-ui/fields": patch -"@object-ui/i18n": patch -"@object-ui/components": patch -"@object-ui/plugin-detail": patch -"@object-ui/permissions": patch ---- - -fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821) - -**The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was -typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any -non-array value through `Object.entries`. Handed the clause string `'name asc'` -— which callers do build by hand — it walked the string index by index and -emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns -that don't exist and every row was filtered out, so -`sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type -and no sharing rule could be created from the Console. `ObjectGrid` builds the -same shape from a schema-level `sort` in three places, so grids with a string -sort silently showed an empty table. - -A string `$orderby` is now passed through verbatim (the server's OData -normalizer has always parsed `'name asc'`), and the type admits `string`. -`RecipientPickerField` additionally switched to the structured -`{ name: 'asc' }` form so it can't regress this way against any data source. - -**The three sharing-rule authoring widgets never had translations.** -`ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded -their English copy — a Chinese Console showed "Select an object", "Select a -user", "Search…", "No matches", "Edit as JSON". They now go through -`useFieldTranslation` like every other widget, with keys added under `fields.*` -in all ten locales. - -The recipient placeholder was the interesting one: it read -`` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum -value into an English sentence — a shape no locale can translate. It is now a -per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so -"选择业务单元" and "Select a business unit" no longer have to share a structure. - -**Editing a rule silently dropped its recipient.** The picker resets the stored -id when `recipient_type` changes, because an id valid for a user is meaningless -for a team. It treated the edit form's `'' → 'user'` hydration as such a change: -opening any saved rule blanked the recipient, and saving persisted the blank. -Only a non-empty predecessor now counts as a type switch. - -**Building a filter submitted the surrounding form.** None of `FilterBuilder`'s -controls declared `type="button"`, and a bare `