Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/11968-flow-fallback-contract-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/app-shell': patch
---

Studio's flow-node inspector, in its fallback form, writes only config keys the node's contract declares (objectui#11968).

The fallback form is the hand-written field table the inspector renders when the engine has published no config schema for a node type: while `GET /automation/actions` is loading, and when that call fails. Two of its groups wrote keys their contract does not declare:

- **HTTP request.** The group offered an **Output variable** row that wrote `config.outputVariable`. The `http` executor never reads that key, its contract does not declare it, and the engine refuses to register a flow that carries it. An author who filled it in on the fallback form saved a flow that then never registered. The row is gone, as it is on the online form, which never offered it.
- **Notify.** The **Click-through URL** row wrote `config.url`, a converted spelling of `actionUrl`. It now writes `actionUrl`, the key the contract declares and the online form writes.

A flow that already stores either key still opens: the key is shown in the Advanced (JSON) block, where it can be cleared, and an unrelated edit keeps it unchanged. A new test holds every fallback group to the keys its node's contract declares, read from the installed `@objectstack/spec`.

Nothing is added to or removed from the package entry.
11 changes: 7 additions & 4 deletions packages/app-shell/src/views/metadata-admin/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6826,7 +6826,8 @@ const FLOW_FIELD_ZH: Record<string, Record<string, FlowFieldZh>> = {
url: { label: 'URL' },
headers: { label: '请求头', help: '请求头(如 Authorization、Content-Type)。' },
body: { label: '请求体', help: '请求负载(JSON 或表达式)。' },
outputVariable: { label: '输出变量' },
// objectui#11968 — no `outputVariable` overlay: the `http` contract declares
// no such key, so neither form offers a field for it.
durable: { label: '持久化(异步)', help: '通过持久化队列即发即忘(异步投递)。' },
signingSecret: { label: '签名密钥', help: 'HMAC-SHA256 密钥 → 写入 X-Objectstack 签名请求头。' },
timeoutMs: { label: '超时(毫秒)' },
Expand Down Expand Up @@ -6954,8 +6955,8 @@ const FLOW_FIELD_ZH: Record<string, Record<string, FlowFieldZh>> = {
errorVariable: { label: '错误变量', help: '在 catch 区域内绑定所捕获错误的变量。' },
retry: { label: '重试' },
},
// notify has no client field table (offline → Advanced JSON); these localize
// the engine-published configSchema fields shown when online.
// notify's offline table and the engine-published configSchema share these
// field ids, so one overlay localizes both forms.
notify: {
recipients: { label: '收件人', help: '接收通知的人:触发记录的某个字段、用户、团队或邮箱地址。至少需要一个。' },
title: { label: '标题', help: '通知标题(别名:subject)。' },
Expand All @@ -6966,7 +6967,9 @@ const FLOW_FIELD_ZH: Record<string, Record<string, FlowFieldZh>> = {
sourceObject: { label: '来源对象', help: '通知所关联记录的对象名。' },
sourceId: { label: '来源记录', help: '通知链接到的记录 id。' },
actorId: { label: '触发用户', help: '引发该事件的用户 id。' },
url: { label: '链接 URL', help: '显式的点击跳转 URL;覆盖默认值。' },
// objectui#11968 — keyed `actionUrl`, the key the contract declares and both
// forms now write; the `url` spelling is the converted alias.
actionUrl: { label: '链接 URL', help: '显式的点击跳转 URL;覆盖默认值。' },
payload: { label: '附加数据', help: '合并进模板输入的额外数据。' },
},
boundary_event: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,20 @@ describe('objectui#11788 — notify recipients renders with the published descri
});

it('an offline row whose key the schema does not declare is not brought back online', () => {
// The offline table's `url` row: the descriptor declares `actionUrl`, and the
// engine rejects an undeclared config key at registerFlow().
// The engine rejects an undeclared config key at registerFlow(). This case
// used to read the offline table's `url` row; objectui#11968 made that row
// write the declared `actionUrl`, and its pin holds every offline row to the
// node's contract, so the real table no longer has such a row. The case is
// read instead off a schema that leaves out a key the offline table has.
const { topic: _topic, ...withoutTopic } = NOTIFY_SCHEMA.properties;
const schema = { ...NOTIFY_SCHEMA, properties: withoutTopic };
const offline = mergeServerFlowFields(null, 'notify');
expect(offline.some((f) => f.id === 'url')).toBe(true);
const online = mergeServerFlowFields(jsonSchemaToFlowFields(NOTIFY_SCHEMA), 'notify', declaredConfigKeys(NOTIFY_SCHEMA));
expect(online.some((f) => f.id === 'url')).toBe(false);
expect(offline.some((f) => f.id === 'topic')).toBe(true);
const online = mergeServerFlowFields(jsonSchemaToFlowFields(schema), 'notify', declaredConfigKeys(schema));
expect(online.some((f) => f.id === 'topic')).toBe(false);
// The lit control: with the full schema the same key IS on the online form.
const full = mergeServerFlowFields(jsonSchemaToFlowFields(NOTIFY_SCHEMA), 'notify', declaredConfigKeys(NOTIFY_SCHEMA));
expect(full.some((f) => f.id === 'topic')).toBe(true);
});

it('without the declared list the merge is what it was: the published fields alone', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ describe('each seeded kind: markers exactly where the spec requires the key (obj
['update_record', ['Object'], ['Filter', 'Field values']],
['get_record', ['Object'], ['Filter', 'Limit', 'Output variable']],
['delete_record', ['Object'], ['Filter']],
['http', ['URL'], ['Method', 'Headers', 'Body', 'Output variable', 'Timeout (ms)']],
// objectui#11968 removed the http group's Output variable row: the `http`
// contract declares no `outputVariable`.
['http', ['URL'], ['Method', 'Headers', 'Body', 'Timeout (ms)']],
['notify', ['Recipients', 'Title'], ['Message', 'Channels', 'Topic', 'Severity', 'Click-through URL']],
['script', ['Function'], ['Inputs', 'Output variable', 'Timeout (ms)']],
['subflow', ['Flow'], ['Input mapping', 'Output variable', 'Timeout (ms)']],
Expand Down Expand Up @@ -210,7 +212,9 @@ describe('the requirement reaches the control, not only the eye (objectui#10948)
expect(screen.getByPlaceholderText('https://api.example.com/v1/contracts').getAttribute('aria-required')).toBe(
'true',
);
expect(screen.getByPlaceholderText('response').getAttribute('aria-required')).toBeNull();
// The optional sibling is the Body text control (objectui#11968 removed the
// Output variable row this read before).
expect(screen.getByPlaceholderText('{ "key": "value" }').getAttribute('aria-required')).toBeNull();
});

it('a required select carries `aria-required` on its trigger', () => {
Expand Down
Loading
Loading