Skip to content

Commit b94ef65

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-n6i8le
2 parents f08f2fa + 7eb613c commit b94ef65

70 files changed

Lines changed: 5569 additions & 3022 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): the security plugin's metadata-derived caches now invalidate on a runtime metadata publish
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The security plugin keeps the per-object inputs it derives from metadata for reuse across requests. Those caches are now dropped whenever metadata changes at runtime, so the next request is judged by the published declaration without a restart (#26596, #26698):
11+
12+
- **The signal.** The plugin listens to the protocol's post-persistence `onMetadataMutation` notification, which every metadata authoring door emits (save, per-item and package publish, revert, rollback, delete) after the engine registry holds the new definition, and which a cluster peer replays after converging its own registry. Drafts are ignored. It also listens to the `metadata:reloaded` announcement that an artifact reload fires.
13+
- **What it replaces.** The previous subscription probed an optional `watch` member on the metadata service, which `MetadataManager` does not implement, and the runtime authoring path does not announce to the metadata service's watchers in any case.
14+
- **Composition note.** A protocol service that offers no `onMetadataMutation` is reported once at `warn` when the plugin starts.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/plugin-approvals': patch
4+
'@object-ui/app-shell': patch
5+
'@object-ui/console': patch
6+
---
7+
8+
feat(spec,plugin-approvals,app-shell): the approvals get serves a request's own route, and both approval step lines draw it
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
The single request read (`GET /api/v1/approvals/requests/:id`) served one step list, `flow_steps`: the flow's trunk, walked from its start by taking the `approve` edge, else the `true` edge, else the first edge. A request a decision routed off the trunk was not on it. A contract routed from the manager to the finance-only rung was served the manager, the joint legal + finance rung, the executive and the general manager, all upcoming, and not its own rung.
14+
15+
What changes:
16+
17+
- `ApprovalRequestRow` gains `flow_route`, served on the same single reads as `flow_steps`. Its `steps` are the approval steps the request's flow run has passed (earlier requests of the same run, on another node, approved, in the order the run opened them, one entry per step), then the request's own step, then, for a pending request, the steps ahead that the flow's graph decides on its own when each step approves.
18+
- Nothing past an undecided branch is listed. The walk ahead stops at a decision node, a conditioned edge, several out-edges, a loop back, or a node that runs other nodes (`subflow`, `map`, `loop`). `ahead` then reads `undecided`. It reads `end` when the walk reached the flow's end, and `decided` for a request that is already decided, whose route ends at itself.
19+
- `flow_steps` is served unchanged. Its doc now says it is the trunk and that a step line never draws it.
20+
- The business record's approvals panel draws the pending request's `flow_route`, so a viewer who took no part in an earlier rung now sees it too. Before, the panel rebuilt the line from the request rows the list read served that viewer.
21+
- The approval request page draws its request's `flow_route`, at any status, where it had stopped drawing a step line at all.
22+
23+
What to do: nothing. A client that reads `flow_steps` reads what it read before. A client that draws a request's progress should draw `flow_route` instead.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
feat(i18n): the `studio` namespace words a refused publish, and a refusal on a dashboard widget or a report input
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Rows for objectstack#26730.** Studio keeps a refused publish on its Pending changes entry until it is fixed, and its "Show me" now ends on the input a refusal names. The namespace gains the sentences for that: a publish refusal (`engine.studio.refusal.publishIssue`, `publishWhere`, `publishPlain`, `publishUnlocated`), the toast that points at the sheet (`engine.studio.publishRefused`, `publishRefusedOne`, `publishRefusedBatch`), and where a dashboard widget or a report's own input is named (`engine.studio.refusal.widget`, `widgetInput`, `reportInput`). Each row has its `zh` text. No existing row changes, and no export changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
feat(i18n): `studio` namespace rows for the Studio nav-item inspector's badge, order and visible-condition fields
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Eleven new `engine.inspector.appNav.*` keys** in `@object-ui/i18n/namespaces/studio`, in `en` and `zh`: the badge and its style (with the four style names the spec's `badgeVariant` declares), the order and its hint, and two hints for the `visible` condition. They label the fields the Studio's Interfaces nav-item inspector now offers for keys a navigation entry already declares (objectstack#26740). Additive: no key changes or goes, no export changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
fix(console): a share link to a non-chat record shows the record's fields as read-only rows, not raw JSON
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The share-link page `/s/:token` printed `JSON.stringify(record)` in a code block for every object except `ai_conversations`: field API names, unformatted values and nested objects as JSON.
11+
- **Now.** The page renders one read-only row per key the resolve answer carries. The label is the key humanized (`annual_revenue` reads "Annual Revenue"). The value is formatted by its JSON shape: ISO dates and date-times in the visitor's locale, numbers grouped, booleans as Yes / No, lists and nested objects as short text, and an empty value as a dash. System fields such as `id` and `created_at` are shown after the business fields. A record with no fields left reads "No data".
12+
- **Unchanged.** The page shows exactly the fields the server returned: redacted fields stay absent and the "Some fields are hidden by the owner" line still renders. The `ai_conversations` transcript, the resolve request, its headers and the password flow do not change.
13+
- **Not yet.** Field labels and field types are not in the resolve answer, and an anonymous visitor cannot read the object's metadata, so authored labels, picklist labels, currency and lookup names do not show yet.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the actions rule, its eval and the i18n skill teach an action's `disabledReason`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `ActionSchema.disabledReason` (the display-only sentence a greyed action shows beside its `disabled` predicate) was on the protocol, but no published skill named it. An agent authoring an action from the skill left the console's default "Not available for this record" in place, or wrote the reason into `description`.
11+
- **Now.** `objectstack-ui/rules/actions.md` has one bullet beside `disabled`: an I18nLabel, display-only, never a predicate or a gate; omitted keeps the console's default, a blank one is refused, and `disabledMessage` / `disabledTooltip` / `disabledHint` / `disabledText` are refused naming the key; the ReassignLead example carries it beside `disabled`. Eval 9 in `evals/views-apps-actions-pages.json` expects `disabledReason` beside a `disabled` predicate and forbids the reason in `description`; cases 5 and 8 are reworded so neither spells its own `must_not_contain` string. The `objectstack-i18n` group table lists `disabledReason` under `_actions.{action_name}` (and so under `globalActions.{action_name}`) beside `confirmText` / `successMessage`.
12+
- No schema, key or behaviour changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(app-shell): Studio's Automations, App and Data creates refuse an identifier a published or draft item of that type already holds
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Before.** "New automation", "Create app" and "New object" saved their blank skeleton as a draft under the first-write pin (`If-None-Match: *` with `?mode=draft`), which the door refuses only where a DRAFT row exists. An identifier a published flow, app or object already held therefore got the skeleton written as that item's pending draft, and publishing the package would have replaced the live item with the blank one. The Data create checked only the objects on its own rail; the other two checked nothing.
12+
- **Now.** Before anything is written, each create reads the published items and the drafts of its type across every package, the read the Interfaces and Access creates already make (now one shared helper). An identifier one of them holds is refused in the dialog with a sentence naming the item; when the item is on the pillar's own rail (a flow or an object of this package), the dialog offers to open it instead. A fresh identifier is created as before, and the door's first-write pin still refuses a draft that lands between the check and the save.
13+
- **Strings** (`@object-ui/i18n`). Five new rows in the `studio` namespace (`en` and `zh`) for the refusals; the Access refusal's "Open" row becomes `engine.studio.create.openExisting`, shared by every create, and the Data pillar's `engine.studio.data.idExists`, which the new refusal replaces, is removed.

‎.github/workflows/ci.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,6 +3418,32 @@ jobs:
34183418
- name: Verify type-check coverage
34193419
run: pnpm --dir ui run type-check:coverage
34203420

3421+
# objectui's `lockfile-integrity.yml` (objectui#8326), row 19 of the
3422+
# objectstack#25697 workflow roster, ported by objectstack#26717: a pull
3423+
# request's `pnpm-lock.yaml` may not give any package a second physical
3424+
# copy. The script's header has the mechanism and its boundary.
3425+
#
3426+
# It lives here because this job already checks out with
3427+
# `fetch-depth: 0`, so `origin/<base>` and the merge base are in the
3428+
# clone, and because it runs on every change to `pnpm-lock.yaml` (the
3429+
# `ui` filter lists it). Before the install on purpose: the gate reads
3430+
# two text files with Node builtins only, which Lint UI's pre-install
3431+
# import graph check holds. `pull_request` only, because the gate's
3432+
# subject is the pull request's delta against its base; a push or queue
3433+
# run has none of its own.
3434+
#
3435+
# ⛔ `continue-on-error` keeps it an ALARM, as objectui ran it: a finding
3436+
# prints an `::error::` annotation naming the packages on the pull
3437+
# request and does NOT fail this required job. Making it blocking is a
3438+
# maintainer decision (`NOT_A_GATE` in `ui/scripts/dependabot-merge-gate.mjs`
3439+
# says why), not a step edit.
3440+
- name: check:lockfile-integrity — compare this pull request's lockfile with its merge base
3441+
if: github.event_name == 'pull_request'
3442+
continue-on-error: true
3443+
env:
3444+
BASE_REF: ${{ github.base_ref }}
3445+
run: pnpm --dir ui run check:lockfile-integrity --base-ref "origin/${BASE_REF}"
3446+
34213447
- name: Install dependencies
34223448
run: pnpm install --frozen-lockfile
34233449

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ Followup to M10.30a. Walked every Setup menu in a real tenant; produced verdict
10301030
- **New `objectstack-formula` skill** ([`skills/objectstack-formula/SKILL.md`](skills/objectstack-formula/SKILL.md)) and rewritten [`content/docs/data-modeling/formulas.mdx`](content/docs/data-modeling/formulas.mdx) — Author-facing documentation of the canonical CEL contract: stdlib reference, mandatory patterns for AI emission, mechanical translation table from the legacy Salesforce-flavor DSL, and the determinism contract.
10311031

10321032
### Removed — M9 Expression Unification
1033-
- **Custom Salesforce-flavor formula engine** (`@objectstack/objectql`) — Deleted `packages/objectql/src/formula.ts` (433 LoC hand-written recursive-descent parser exposing 22 functions: `CONCAT`/`UPPER`/`IF`/`AND`/`OR`/`ISBLANK`/`TODAY`/…) and its companion `packages/spec/docs/formula-functions.md`. The engine had no formal grammar, no public training corpus, no AST persistence, and was a single-vendor surface. **Salesforce compatibility is not pursued** — see [north-star §8](content/docs/concepts/north-star.mdx) "No private expression DSL". Authors targeting Salesforce semantics rewrite their formulas in CEL using the translation table in the formula skill. Resolves D9.
1033+
- **Custom Salesforce-flavor formula engine** (`@objectstack/objectql`) — Deleted `packages/objectql/src/formula.ts` (433 LoC hand-written recursive-descent parser exposing 22 functions: `CONCAT`/`UPPER`/`IF`/`AND`/`OR`/`ISBLANK`/`TODAY`/…) and its companion `packages/spec/docs/formula-functions.md`. The engine had no formal grammar, no public training corpus, no AST persistence, and was a single-vendor surface. **Salesforce compatibility is not pursued** — see [north-star §8](https://objectstack.ai/docs/concepts/north-star) "No private expression DSL". Authors targeting Salesforce semantics rewrite their formulas in CEL using the translation table in the formula skill. Resolves D9.
10341034

10351035
### Added
10361036
- **Per-organization seed-data clone for new signups** (`@objectstack/plugin-security`) — In multi-tenant mode, when a user signs up after the very first user (whose org gets the seed via `claimOrphanTenantRows`), SecurityPlugin now copies the platform-first organization's user-defined data into the new user's personal workspace via `cloneTenantSeedData()`. Without this, every user-after-the-first landed on an empty dashboard — multi-tenant isolation is correctly enforced, so seed rows owned by the first org are filtered out by RLS for everyone else. Implementation: two-pass strategy — pass A inserts shallow row copies (lookup field values left as donor's old IDs) under the new `organization_id` and records `oldId → newId` per object; pass B walks each cloned record's lookup fields and rewrites values via the per-object remap so intra-clone foreign keys (e.g. `opportunity.account → account.id`) stay intact without needing topological insert ordering. Skips computed/virtual field types (`formula`, `summary`, `autonumber`) that have no underlying SQL column. Disambiguates `unique: true` columns with a per-tenant `clone-<orgsuffix>-` prefix so global UNIQUE indexes (e.g. `lead.email`) don't collide. Idempotent per-object — if the target org already has any row in an object, that object is skipped. Donor org is the oldest `sys_organization`. Best-effort: a per-object failure is logged at `warn` and never blocks signup. Wired via DI hook in `ensureUserHasOrganization` so the security plugin owns the policy and the helper stays unit-testable. Verified end-to-end with `pnpm dev:crm`: 2nd registered user gets 3 leads, 5 accounts, 4 opportunities, 5 campaigns, 4 products, 5 tasks, 2 cases, 1 contract cloned into their personal workspace.

‎content/docs/automation/approvals.mdx‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,29 @@ It is computed from the open-time approver snapshot (OOO substitution applied),
873873
so it reflects who **actually** still needs to sign — the console renders it as
874874
per-group tick badges or a "2 of 3 · finance pending" count.
875875

876+
The same read serves the request's own **route**, `flow_route`, which both
877+
approval step lines draw (the record's approvals panel and the request page):
878+
879+
```jsonc
880+
// getRequest, for a contract a gateway routed from the manager to finance:
881+
"flow_route": {
882+
"steps": [
883+
{ "id": "manager_review", "label": "Manager", "state": "done" },
884+
{ "id": "finance_only", "label": "Finance", "state": "current" }
885+
],
886+
"ahead": "undecided" // a gateway follows: nothing past it is listed
887+
}
888+
```
889+
890+
`done` steps come from the run's own request rows (earlier requests of the same
891+
run, approved), never from the flow graph. Steps `upcoming` are listed only for
892+
a pending request, and only as far as the graph decides them when each step
893+
approves: the walk stops at a `decision`, a conditioned edge, several
894+
out-edges, a loop back or a `subflow` / `map` / `loop` node, and `ahead` reads
895+
`undecided`. `ahead` is `end` when the walk reached the flow's end, and
896+
`decided` for a request already decided. The older `flow_steps` member is the
897+
graph's first branch at every decision, not the route; nothing draws it.
898+
876899
Approval **notifications deep-link into the request**: `notify()` rewrites the
877900
inbox action URL to `/system/approvals?request=<id>`, so clicking the bell opens
878901
that request's drawer directly instead of a generic list.

0 commit comments

Comments
 (0)