Repository navigation
Commit 6d9d3ef
feat(spec,automation,app-shell)!: a flow screen field typed
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb7z92P2z3yYaoSGUvsM82Eb)_
Part of #25801
Clause-②: yes (narrowing)
Metadata-protocol: semantic
## Summary
This PR does the card's first half: a flat `screen` can ask for a file,
so a wizard collects the file before the record that will own it exists
and writes everything after its last screen. The card's second half
(closing a paused run's dialog cancels the run, or offers to) is left
open as a product decision. It is not built here, and the section at the
end says why.
A screen field declared `type: 'file'` now works like this:
```ts
{ id: 'ask', type: 'screen', config: { fields: [
{ name: 'contract_name', label: 'Name', type: 'text', required: true },
{ name: 'contract_file', label: 'Signed copy', type: 'file', required: true },
] } },
{ id: 'write', type: 'create_record', config: { objectName: 'contract',
fields: { name: { dialect: 'cel', source: 'contract_name' }, doc: { dialect: 'cel', source: 'contract_file' } } } },
```
- **Console.** The flow screen dialog renders the object form's own
`FileField` for the field, reached through `getLazyFieldWidget('file')`
as `ActionParamDialog`'s file params reach it. It uploads through the
console's `UploadProvider`, which is the file service. The upload lands
as a `sys_file` that no record owns yet, and the widget's value is that
file's id. The screen body renders inside an uploading scope
(`useUploadingScope`), so while an upload is in flight Submit is
disabled and reads "Uploading…". The resume posts
`screenResumeValues(...)`, which reduces each file field's value to its
bare id with `fileIdOf` (a prefilled `defaultValue` may arrive in the
expanded `{ id, name, url, … }` form).
- **Engine.** `validateScreenInputs` reads `'file'` as the second closed
value domain beside `'number'`. A value that is not a file id is refused
with `400 INVALID_SCREEN_INPUT` and the field code `invalid_type`
(`Screen field "contract_file" must be the id of an uploaded file`). The
id is judged by the spec's `isFileIdToken`, the same predicate the claim
path reads, so whatever the door admits is something a later write can
claim. A URL, the expanded read form, a list or a number is refused and
never reduced. The refusal writes nothing and keeps the pause, so a
corrected resume still lands. An empty optional field and a hidden field
are not judged.
- **Spec and docs.** `ScreenFieldConfigSchema.type` and the
`ScreenFieldSpec` contract document the new member, the reference docs
are regenerated, and the flows guide gains an "A file field" section.
The key stays an open string, so nothing an author already wrote is
refused at publish.
- **Ownership.** The `create_record` after the screen writes the id into
the record's `file` field, and that write claims the file for the record
(ADR-0104 D3: one file, one owner, met by unattached-then-claimed).
Under the default `runAs: 'user'` the claim first makes the download
door's read check as the run's user (#25692). This PR does not touch
`service-storage`: the claim is the lifecycle #25951 landed, unchanged.
## Tests
Measured on this branch.
- `service-automation`: `screen-input-file-field.test.ts` (new) covers
minted ids accepted, six wrong shapes refused with the exact message,
agreement with `isFileIdToken` on every shape, absence left to
`required`, a hidden field not judged (with a positive control), and
`image`/`avatar`/`video`/`audio` left open.
`builtin/screen-file-field-resume.test.ts` (new) runs the engine: the
pause carries `type: 'file'`, a URL resume is refused with nothing
inserted and the pause kept for the corrected resume, and an id resume
reaches `create_record` unchanged.
- `app-shell`: `FlowRunner.fileField-25801.test.tsx` (new, 7 tests)
drives the real `FileField` under a held `UploadProvider` adapter. It
covers the widget rendering instead of a text input; Submit disabled and
reading "Uploading…" mid-upload, a click on it resuming nothing, and the
resume after the upload settles posting `{ contract_file: MINTED_ID }`;
a required empty file field refused locally; a prefilled expanded value
posted as its id; and `screenResumeValues` on its own. With its
neighbours `FlowRunner.bounds-9248` and `ActionParamDialog.uploading`:
Test Files 3 passed (3), Tests 16 passed (16).
- Dogfood: `flow-screen-file-intake.dogfood.test.ts` (new) boots a real
stack with the local storage adapter and drives it as a plain member.
The paused screen carries `type: 'file'`; an upload lands as a
`sys_file` with no owner; a resume carrying the download URL or the
expanded form is refused `400 INVALID_SCREEN_INPUT` with no record
written and no claim; the corrected resume lands on the same run; and a
resume with the id writes the record with the id in its `file` field and
claims the file (`ref_object`, `ref_id`, `ref_field`), read back through
the data door.
`pnpm --filter @objectstack/dogfood exec vitest run
test/flow-screen-file-intake.dogfood.test.ts`: Test Files 1 passed (1),
Tests 4 passed (4). The full battery at `1e9a122b`: Test Files 297
passed, 3 skipped (300); Tests 2527 passed, 18 skipped (2545). The
commits after it change only the changeset and the flows guide.
- Type-check: `pnpm --filter @object-ui/app-shell run type-check` and
`pnpm --filter @objectstack/service-automation run typecheck`, exit 0
each. `pnpm --filter @objectstack/dogfood run typecheck`, exit 0.
## Ablation
Each mutation went through `scripts/ablation-replace.mjs` with its
restore-to-HEAD check:
- `service-automation`: deleting the `'file'` row of the value-domain
map turned 4 tests red. The file was restored to its HEAD blob
`753e36a8`.
- `FlowRunner.tsx`: `disabled={submitting || uploads.anyUploading}` to
`disabled={submitting}` turned 1 test red, the held-upload test, on
`toBeDisabled()`.
- `FlowRunner.tsx`: `resumeWith(screenResumeValues(screen, values))` to
`resumeWith(values)` turned 1 test red, the prefilled expanded value
test.
- `ScreenView.tsx`: `if (kind === 'file') {` to `if (false) {` turned 4
tests red, every dialog test that waits for the widget's file input.
Each file was restored with `git diff HEAD` empty. A first attempt at
the `disabled` mutation split its anchor on the `||` and wrote a file
that did not parse ("no tests"). That reading was void and was re-run
with the full anchor; the line above is the re-run.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `9c17572c` (117 commands). Each ran with
its exit code captured before any pipe, and `--ran` reconciled them:
"117 derived famil(ies) accounted for — 116 run, 1 NOT-MEASURED". The
116 that ran exited 0. They include `check-adr-0087-registration`,
`check-changeset-no-major`, `check:api-surface`,
`check:authorable-surface`, `check:generated`, `check:docs`,
`check:liveness`, `check:nul-bytes` and `check:type-check-debt`, which
ran under the verify lock and reported "26 raw tsc error(s) total, none
above its recorded number".
Not measured locally, left to CI: `check:dual-build-cjs-loads`, which
needs a full workspace `pnpm build`. It stopped on that prerequisite
with exit 3 before reading anything. This diff does not change any
package's build shape.
## Acceptance notes
- **The second half of the card is a product decision, not built here.**
The cancel door (`POST /automation/:name/runs/:runId/cancel`) is an
operator verb (#13953: "a run belongs to the environment rather than to
a user"), so a run's starter who is not an operator gets 403
`PERMISSION_DENIED` there. A cancel also does not undo writes made
before the screen. Closing the dialog therefore cannot cancel the run
without a new starter-scoped verb or a wider cancel gate, and "cancel
silently" against "offer to cancel" is a real fork. It is reported to
the seat with options; this PR keeps today's behaviour (closing leaves
the run paused). With the first half in, a wizard can write after its
last screen, so a closed dialog leaves no half-written record.
- **BREAKING, a resume-acceptance narrowing.** The resume now refuses
values it used to accept for a field already declared `type: 'file'` (a
URL, the expanded form, any non-id string), and that `type` value
changes meaning from a text input to an upload. No producer of a `type:
'file'` screen field was found in the repo's examples or packages. The
seat amended the claim's declaration lines to match (#25801, comment
6417676103). The changeset carries the FROM/TO fix for a caller that
resumes the run itself, and the ADR-0087 disposition `not-required
(no-migration-prescription)`: no spec key, stored row or export moves,
so there is nothing for `objectstack migrate meta` to rewrite.
- A `runAs: 'system'` screen flow writes the user-supplied id without a
read check, because no system write is checked (the posture #25951
decided). This is not new: a system flow could already write a `type:
'text'` screen value into a `file` field. The spec docblock and the
changeset say so.
- The console lowercases a screen field's `type` before choosing a
widget, while the engine matches the value domain exactly. A field
declared `type: 'File'` gets the upload widget but no server check. This
asymmetry already exists for `'number'`. Noted, not filed.
- One file per field. A screen field has no `multiple`, `accept` or
`maxSize` key, so a list is refused at the resume and the widget applies
no type or size limit. The target object's own `file` field constraints
still apply at the write after the screen, which fails the run there if
the file breaks them. Adding those keys is a widening for its own card.
Noted, not filed.
- `image`, `avatar`, `video` and `audio` screen hints still render a
text input and stay open widget hints. The value-domain set is closed
and grows by ruling.
- This PR does not touch
`ui/packages/app-shell/src/utils/flowResponse.ts` (PR #26011) or
`service-storage` (sibling #25779).
---
_Generated by [Claude
Code](https://claude.ai/code/session_0121LS2jqNju6opaDpdcn7ht)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_0121LS2jqNju6opaDpdcn7ht
---------
Co-authored-by: Claude <noreply@anthropic.com>file collects an upload, and the run resumes with the uploaded file's id (#26134)1 parent dfaad4d commit 6d9d3ef
14 files changed
Lines changed: 959 additions & 48 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- packages
- qa/dogfood/test
- fixtures
- services/service-automation/src
- builtin
- spec/src
- automation
- contracts
- ui/packages/app-shell/src/views
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
785 | 785 | | |
786 | 786 | | |
787 | 787 | | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
788 | 821 | | |
789 | 822 | | |
790 | 823 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
281 | | - | |
| 281 | + | |
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
303 | | - | |
| 303 | + | |
304 | 304 | | |
305 | 305 | | |
306 | 306 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
0 commit comments