Skip to content

fix(workflow-form): let ui-udf-parameters show the form's renames and hides in its columns, and add a parameter to the card's operator - #8846

Open
yangzhang75 wants to merge 4 commits into
apache:mainfrom
yangzhang75:fix/form-udf-parameters-overrides
Open

yangzhang75 wants to merge 4 commits into
apache:mainfrom
yangzhang75:fix/form-udf-parameters-overrides

Conversation

@yangzhang75

@yangzhang75 yangzhang75 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Closes #8763. Part of the Form View feature (#8011); from the #8439 refinements (item 6).

ui-udf-parameters (the Python UDF parameter table) is exposable, but its renderer hides formly's labels and draws fixed column headers (Value / Name / Type), so the author's per-sub-field overrides from the form (#8438) changed nothing a reader saw: formly expresses a rename as the sub-field's label and a hide as hide, and renders both through the field's wrapper, which this widget's cells never go through -- a renamed column kept its old heading, a hidden one kept its heading over an empty cell. Reviewing it turned up more the widget got wrong on a Form View card. The operator property panel is untouched throughout.

  • The form publishes what the author decided; the widget asks. New common/formly/sub-field-view.ts: a host that holds per-sub-field decisions publishes them once on the field it hands a widget (props.subFieldView, keyed by sub-field path: { label, shown } -- the author's name, or "" where they kept the one it came with, and whether a reader sees the part at all), and a widget that draws its own chrome asks subFieldsInView / subFieldLabel. Reading the state back out of the rendered rows was not the answer: it is only there once formly has built a row, so a table whose UDF declares nothing yet had nothing to read, and a group's hide is marked on the group while its children are merely not rendered. The Form View fills the map during its override walk (the first node met for a path answers for it, as it does for the controls: the rows of a repeated section share one override), shown for an author always (who keeps a hidden sub-field on screen, faded, to bring it back) and for a reader unless the override hides it; a hide of the attribute group takes both its columns out. A name being typed is written into the very object the field carries, so a header follows the typing without a rebuild; an emptied box deletes the override and the header falls back to the widget's own name. Nothing published -- the panel, where there is no author -- means every part in view under the widget's own name, which is what its fixed headers already say.
  • The widget's columns and headers follow it on a card. columns is read live (the form hands the widget its field after the widget's first check) through subFieldsInView, which hands back the same array instance while the answer has not changed so the header's ngFor does not redraw; a header reads subFieldLabel with the fixed label as fallback -- the author's name, else "Type", never the schema's "Attribute Type", which the panel does not show either. All three columns come back while the draft row is open: that row names and types the parameter being added, so it needs them whatever the author hid (Copilot on this PR: a filtered header over its three cells put the name box under the wrong heading).
  • A column's label controls live in the header, once (second commit; found on the running instance while authoring). The form's label wrapper decorates every row's sub-fields -- the first row with the name box and the eye, every later row with a static name -- and this widget drew them in its cells under the header that now reads the author's name, so a column's name was drawn three times over (header, first-row box, each later row's static label; and the static one said "Attribute Type" where the header said "Type"). The wrapper gains one prop, labelHosted: a host that draws the label itself sets it, and the wrapper then renders no box, eye or static label, only the control's accessible name (sr-only, tied to the control by for, or the group twin for a repeated field) and the dimmed state. The widget hosts the one name box and eye per column in its header, bound to the first row's props and callbacks (authorName / authorHidden / renameField / setFieldHidden -- the row the form gave the controls to), and for each column it hosts marks every cell of that column, named as the heading reads. The write-through those controls make (the props first, then the form) and the box's and eye's styles have one definition each, on the wrapper (EditableLabelWrapperComponent.rename / setHidden; _editable-label.scss), which the header uses rather than copies. How a later row's own name keeps up with a rename is the form's followersByPath, as before. A renderer with no header of its own (the generic array widget) sets nothing and is untouched; the panel carries no operator mark, so nothing is hosted or marked there.
  • The form names the operator on every field it renders (props.operatorID: FIELD_OPERATOR_ID_PROP / fieldOperatorID / renderedInFormView in custom-formly-type.ts), since nothing is highlighted on the form; the same mark tells a shared widget which host it is on, where the two must differ.
  • "Add parameter" works on a card as on the panel. It inserts a declaration into an operator's code, and it took that operator from the canvas highlight -- on the form nothing is highlighted, or another step is, so it edited the wrong operator or failed with "code is not available". It now adds to the field's operator on a card and to the highlighted one on the panel. Whether a card offers it (editable) asks the two halves of the canvas's rule separately, because the canvas's flag answers neither one on that page. Write access: the card is not a reader's, which the form marks on the field itself (props.disabled; renderedReadOnly). So a writer's card offers it in and out of edit mode, as every value on the card can be edited, and adding a parameter is adding a row; it followed the canvas's modification flag, which the Form View holds off outside edit mode against structural edits through its page, so a writer could edit every value on the page but not add a parameter to this table without entering edit mode. No run in flight: asked of the execute service, which owns that rule (isExecutionInFlight(), lifted out of the switch that already locks the graph, so the states are listed once). Adding a parameter writes into the UDF's code, and code is what a run must not have rewritten under it. The mark decides rather than the table's own control, which does not answer the question: formly disables the leaf controls under a disabled field, not the array itself, so a reader's table with no parameters yet reads as enabled and would have offered the button. The check also sits in the method, not only on the button. The panel keeps the canvas's rule, the flag, unchanged.
  • The declared parameters then reach the operator. The parser announces the UI parameters a UDF's code declares, and the operator property panel's frame writes them onto the operator for the operator it shows, only while it acts as the editor. With the panel closed or on another step nobody wrote them, so a parameter added from a card changed the code but no row appeared. The form writes them when the frame does not: a writer only, not while the step panel shows that very step live, and only for an operator still on the graph; the same value the frame writes. The card showing them is then rebuilt right after the current tick (once every writer has written; held while the reader is typing, and flushing a card's pending write first, as every rebuild is since fix(workflow-form): rebuild the cards on compilation only when it changed something they show #8845): it would otherwise have kept its old rows until the compilation result rebuilt it, half a second and a round trip later, and a value typed into it in that window would have gone back with the old rows, dropping the one just declared.
  • A read-only reader's value cells are locked: the Form View marks a reader's field props.disabled, and the widget's own "the value is editable" used to re-enable the control after formly had disabled it.

Any related issues, documentation, discussions?

Closes #8763. Raised by Copilot on #8438; filed from #8439. Rebased on main, which now has #8848 (the widget's lock on the Name and Type cells holding under the panel's wholesale enable(), split out of an earlier revision of this PR) and #8845 (a card's pending write is flushed before every rebuild, which the catch-up rebuild here inherits; an earlier revision listed that as #8844, open then). #8760 (the form's own code editor host) needs the same operator mark and the same write of the declared parameters; whichever lands second drops its copy. Checked under real formly while reviewing: hiding the attribute group itself keeps the rows' names and types in the model (formly resets a hidden field's children only on a hide that happens after the build; the form sets hide at build only, and a reader's card is rebuilt rather than toggled). Table semantics for assistive tech (the header and cells are plain divs, as on the canvas) stay a follow-up. The remaining stale-row window is a collaborator declaring a parameter while this user types in that same card, where the rebuild is held and the card's write-back puts the old list back; closing it means a card writing only its own key into the operator's current value instead of replacing the whole property, which is a change to every property's write-back and belongs in its own PR.

Seen in review, the Form View's own and not changed here: the form's array widget (ArrayTypeComponent) shows its add and remove buttons to a reader too, without a write-back behind them; that goes to a follow-up issue. And on main, renaming or hiding a repeated section's sub-field reaches only its first row at once: followersByPath registers the configs the section's builder returns, which formly clones before rendering, so the rows on screen never hear of the edit until a rebuild shows the saved override on every row. Pre-existing, found while checking the second commit, which hosts the first row's controls and leaves that mechanism as it is; it goes to its own issue.

An earlier revision of this PR let a run through. It described a writer's cards as editable mid-run and read the button as following from that, which holds for the values -- they were always editable here -- but not for this button: the canvas's flag had been hiding it during a run, and dropping the flag dropped that with it. A declaration goes into the UDF's own code, straight into the shared document and not through the graph's lock, and the property panel and the canvas code editor both refuse a code edit while a run is in flight (mengw15). The card now asks for that half on its own; see the rule above.

How was this PR tested?

Unit tests (vitest), added or extended in this PR; every statement, function and branch the PR adds is covered, with the rule for Add parameter pinned once, under real formly, rather than again by hand.

  • sub-field-view.spec.ts: a host that publishes nothing leaves every part in view under the widget's own name; a publishing host drops what the author hid and keeps the rest in the widget's order, gives the author's name and the widget's own where the author kept it, and keeps, and names, a part it did not mention; the same array comes back while the answer has not changed and a new one once it has, each host's answer kept apart; a published value that is not a map is ignored.
  • The widget's spec: it is on the panel until formly hands it a field and on the form once the field names its operator (and hosts no controls before then); a renamed column shows under its new name and a hidden one is left out, in the header and the cells; the three fixed columns come back while the draft row is open; rows formly builds on demand; a hidden group takes its columns out; the author's name shows while renaming and follows the typing; a table whose UDF declares nothing yet; the panel keeps its fixed headers whatever the sub-fields carry, and the fixed name for a column the author did not rename whatever the schema calls it; the columns are read live, and the same list comes back while nothing changed; a reader's value cells are locked; Add parameter adds to the card's operator on a writer's card, the canvas's flag off, and on the panel to the highlighted one; on a card that is not editable (a reader's, or a writer's while a run is in flight) the method adds nothing, whoever calls, and the draft stays open; on the panel nothing is added while modification is off. Second commit: the header hosts one name box and one eye per column, seeded from the first row, and every cell of every row is marked and named as the heading reads; typing in the header renames through that column's first-row callback and the box follows, the other columns' callbacks untouched; the eye hides through that column's callback, the column fades and stays in view and the eye says so, a second click un-hides; no eye for a column the form marked not hideable, and the mark survives a re-populate; a reader's card and the panel keep a plain text header with no cell marked.
  • The widget's formly spec (the table stood up under FormlyModule.forRoot, a builder row template as formly-jsonschema produces, nz-input cells as in the app, so the rows are formly's own and a reader's lock is the one formly applied): a writer's Form View card offers Add parameter without the canvas's flag and a reader's card, its field disabled, does not; the same with no parameters yet, where formly leaves the empty array control enabled; a run in flight takes the button off a writer's card and leaves the values editable.
  • The wrapper's spec: with labelHosted, the row that carries the controls draws no box or eye but is still named and still fades when hidden; a later row draws no static name, only the sr-only one tied to the control by for, or the group twin with its -label id for a repeated field; rename / setHidden write the props and then call the form, and tolerate a reader mount that has neither callback.
  • The form's spec: every field carries its operator; the declared UI parameters are written for a writer when the step panel is not editing that step, left to the frame while it shows the step live (the card still rebuilt), written when it shows the step read-only, and not written for a reader or a vanished operator; the cards are rebuilt after the current tick when one shows that step's parameters, through buildForm (the old cards cut off, the card holding the declared rows), held while the reader is typing and run once the focus leaves, and left alone when none shows that step's parameters or the one that does is another step's; a sub-field formly built without props is renamed; a name being typed lands in the published view on the field itself, a rename for a path the view does not carry is ignored, an author is told a hidden sub-field is still in view and a reader that it is not, and no name is published for a sub-field the author did not rename.
  • The execute service's spec pins isExecutionInFlight against the graph lock, state by state; custom-formly-type's spec pins fieldOperatorID, renderedInFormView and renderedReadOnly.

eslint, prettier and the production (AOT) build pass; the full frontend suite passes.

Checked on a flag-on instance (main-demo; a Python source feeding a Python UDF whose code declares two UI parameters, uiParameters exposed with the Name column renamed "Parameter" and the Type column hidden), the canvas and the form open side by side, main against this branch. On main the card shows Value / Name / Type, the overrides ignored. With this PR the card shows Value / Parameter, the Type column gone, while the canvas panel still shows Value / Name / Type. A writer's card offers Add parameter in and out of edit mode (on main it appeared only in edit mode, while any list's rows could be added either way); adding cutoff (double) inserts self.UiParameter(name="cutoff", type=AttributeType.DOUBLE) into that UDF's code, and the new row appears on the card and in the canvas tab's panel within a few seconds, and is there after a reload. In edit mode, the first commit alone drew 3 header titles, 3 name boxes and 3 eyes in the first row and 3 static labels in the second; with the second, the header holds the 3 name boxes and 3 eyes and the cells hold none, and a hidden column's header fades with its cells.

The canvas panel shows the same three columns, the same values and the same behaviour as on main, side by side on both instances.

Video

Before

Screen.Recording.2026-10-04.at.10.13.07.PM.mov

After

Screen.Recording.2026-10-04.at.10.13.22.PM.mov

Was this PR authored or co-authored using generative AI tooling?

Yes. Generated-by: Claude Code (Claude Fable 5.1, Anthropic). Co-authored with Claude, reviewed line by line by the author before submission.

🤖 Generated with Claude Code

@github-actions github-actions Bot added fix frontend Changes related to the frontend GUI labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @carloea2, @pedroreyesalvarado
    You can notify them by mentioning @carloea2, @pedroreyesalvarado in a comment.

@Yicong-Huang Yicong-Huang added release/v1.3 back porting to release/v1.3 release/v1.2 back porting to release/v1.2 labels Oct 5, 2026
@github-actions
github-actions Bot requested review from mengw15 and xuang7 October 5, 2026 02:17
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Backport auto-label report

This fix: PR was checked against each actively-supported release branch. A release/* label nominates a backport target; the branch's release manager approving this PR is what sends the fix there. The required Backport Approvals check stays red until every label below is approved, so each manager either approves or removes their own label — which is why the labels left on a merged PR are exactly the branches it reached.

Release branch Analysis
🚫 release/v1.3 Label was removed earlier (opt-out); not re-added. Re-add it by hand if this fix should be backported here after all — the branch's release manager then has to approve before this PR can merge.
🚫 release/v1.2 Label was removed earlier (opt-out); not re-added. Re-add it by hand if this fix should be backported here after all — the branch's release manager then has to approve before this PR can merge.

Auto-label run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Filtered column headers mislabel the fixed Add parameter draft controls when Name or Value is hidden.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Updates Form View’s UDF parameter tables to respect column overrides and add parameters to the card’s operator, while preserving property-panel behavior.

Changes:

  • Applies column renames, visibility overrides, and read-only locks.
  • Routes parameter additions to the bound operator and refreshes affected cards.
  • Adds unit and real-Formly regression coverage.
File Description
frontend/​src/​app/​workspace/​util/​custom-formly-type.ts Adds operator and read-only field helpers.
frontend/​src/​app/​workspace/​util/​custom-formly-type.spec.ts Tests field helpers.
frontend/​src/​app/​workspace/​component/​workflow-form/​workflow-form.spec-harness.ts Adds parameter-sync mocks.
frontend/​src/​app/​workspace/​component/​workflow-form/​workflow-form.rendered.spec.ts Provides the parameter-sync dependency.
frontend/​src/​app/​workspace/​component/​workflow-form/​workflow-form.component.ts Marks fields and synchronizes declared parameters.
frontend/​src/​app/​workspace/​component/​workflow-form/​workflow-form.component.spec.ts Tests synchronization, rebuilds, and field metadata.
frontend/​src/​app/​workspace/​component/​ui-udf-parameters/​ui-udf-parameters.component.ts Handles overrides, operator targeting, and edit permissions.
frontend/​src/​app/​workspace/​component/​ui-udf-parameters/​ui-udf-parameters.component.spec.ts Tests Form View and panel behavior.
frontend/​src/​app/​workspace/​component/​ui-udf-parameters/​ui-udf-parameters.component.html Renders filtered columns and updated headers.
frontend/​src/​app/​workspace/​component/​ui-udf-parameters/​ui-udf-parameters.component.formly.spec.ts Tests permissions and controls under real Formly.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@yangzhang75
yangzhang75 force-pushed the fix/form-udf-parameters-overrides branch from 49e5528 to 94aff48 Compare October 6, 2026 18:01
@codecov-commenter

codecov-commenter commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.02985% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.78%. Comparing base (cfca8e1) to head (e643084).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
...abel-wrapper/editable-label-wrapper.component.html 66.66% 0 Missing and 4 partials ⚠️
...t/ui-udf-parameters/ui-udf-parameters.component.ts 92.10% 0 Missing and 3 partials ⚠️
...component/workflow-form/workflow-form.component.ts 96.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8846      +/-   ##
============================================
- Coverage     91.81%   91.78%   -0.03%     
  Complexity     5019     5019              
============================================
  Files          1277     1281       +4     
  Lines         56369    56787     +418     
  Branches       7364     7476     +112     
============================================
+ Hits          51755    52123     +368     
- Misses         2759     2793      +34     
- Partials       1855     1871      +16     
Flag Coverage Δ *Carryforward flag
access-control-service 77.44% <ø> (ø) Carriedforward from a354759
agent-service 99.53% <ø> (ø) Carriedforward from a354759
amber 85.78% <ø> (ø) Carriedforward from a354759
computing-unit-managing-service 64.05% <ø> (ø) Carriedforward from a354759
config-service 87.73% <ø> (ø) Carriedforward from a354759
file-service 82.31% <ø> (ø) Carriedforward from a354759
frontend 96.40% <94.02%> (-0.14%) ⬇️
notebook-migration-service 83.77% <ø> (ø) Carriedforward from a354759
pyamber 98.50% <ø> (ø) Carriedforward from a354759
workflow-compiling-service 78.30% <ø> (ø) Carriedforward from a354759

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues remain, and regression coverage includes the corrected draft-column alignment.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@yangzhang75
yangzhang75 force-pushed the fix/form-udf-parameters-overrides branch from 94aff48 to 8f86ca9 Compare October 7, 2026 00:59
@yangzhang75
yangzhang75 force-pushed the fix/form-udf-parameters-overrides branch from 8f86ca9 to 01ace8f Compare October 17, 2026 00:25
@mengw15 mengw15 removed release/v1.2 back porting to release/v1.2 release/v1.3 back porting to release/v1.3 labels Oct 18, 2026

@mengw15 mengw15 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left one comment

@yangzhang75
yangzhang75 force-pushed the fix/form-udf-parameters-overrides branch from 01ace8f to 11d0214 Compare October 18, 2026 23:39
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Nov 5, 2026
…lls locked when the panel enables its form group (apache#8848)

### What changes were proposed in this PR?

Closes apache#8847. The Python UDF operator's "UI parameters" table (apache#5043)
infers each parameter's name and type from the `self.UiParameter(...)`
declarations in the operator's code, so the widget disables those two
columns' controls as it builds each row: a name typed into the table
does not reach the code, and the parameter's value then never reaches it
either (checked on main: the table's name changes, the code's does not).

The lock does not survive on the operator property panel.
`setInteractivity(true)` enables the whole form group -- one task after
every render, and again whenever the workflow becomes modifiable, for
instance when a run ends -- and formly mirrors a control's state back
into `props.disabled`, so the one-time disable is simply undone. Which
state a given panel opening shows depends on which of the two ran last:
measured on main, the cells were editable on 4 of 5 openings, and always
after a run. The Form View, which never enables a group wholesale, has
always shown them locked, so the two views disagreed as well.

The widget now holds its own lock. On each cell it locks it registers
Angular's disabled-change hook of the control
(`registerOnDisabledChange`, the same hook formly's mirror and the
input's value accessor use): when a host enables the control, the hook
disables it again in the microtask after that enable, and announces the
status change as the enable did. Both details matter and were found on a
running instance:

- deferred to the microtask, because the input's value accessor is one
more hook on the same control, registered after this one when the cell
renders; disabling from inside the enable's own round of hooks left the
accessor having been told "enabled" last, so the box was editable over a
locked control;
- announced rather than silent (`disable()`, not `disable({emitEvent:
false})`), because ng-zorro's `nz-input` paints its `disabled` attribute
from the control's status changes; a silent disable left it painting the
box editable again at the next change detection.

A cell the widget configures editable -- the Value column, or any cell
once reconfigured -- is released from the hold, so whether it is
editable stays the host's call: a read-only panel still locks
everything, and a host that disables the whole field still reaches the
Value cell. The frame is untouched: two other approaches were tried
first and dropped, re-disabling from the frame after `enable()`
(formly's mirror has already set `props.disabled` to false by then) and
a formly expression on `props.disabled` (formly re-evaluates expressions
on model changes only, and registers them at populate, so `enable()`
never triggers one).

### Any related issues, documentation, discussions?

Closes apache#8847. Split out of apache#8846 (the same table under the Form View's
renames and hides), which is based on main and does not touch this; the
two are independent and can land in either order. The table's fixed
250px columns clip Name and Type at the panel's default 260px width,
which is why the cells have to be brought into view to see any of this;
that is a separate layout bug, filed separately.

### How was this PR tested?

Unit tests (vitest). The widget's spec: a locked cell's control is held
locked when a host enables it, in the microtask after the enable and
announcing the status change, while the Value cell is left to the host;
a cell configured editable again is released from the hold; a cell the
host disables again before the microtask is left alone; a control that
is not a `FormControl` gets no hook, and a control is hooked once
however often it is configured. A second spec stands the table up under
real formly (`FormlyModule.forRoot`, a form, the builder row template
formly-jsonschema produces, `nz-input` cells as in the app): the rows
build with Name and Type locked and Value editable; a wholesale
`form.enable()`, as the panel does, leaves Name and Type locked, read
from the inputs' `disabled` state and not only from the controls'; a
read-only `form.disable()` locks everything and a later `enable()` gives
only the Value cell back. Each guard was deletion-checked and turned a
named test red. eslint, prettier and the production (AOT) build pass;
the full frontend suite passes; every statement, function and branch
this PR adds is covered.

Checked on a local instance (a Python source feeding a Python UDF whose
code declares two UI parameters), the panel widened so the columns are
in view. On main the cells are editable on most openings (4 of 5) and
after every run; with this PR they stayed locked on every opening and
through a forced `setInteractivity(true)` followed by real
change-detection passes, read from the DOM as well as from the controls.
The Value cell follows the host in both.

#### Video

**Before**


https://github.com/user-attachments/assets/53ac4826-b5ec-4852-a5c9-22685306839d



**After**


https://github.com/user-attachments/assets/9bcc2a52-fdb3-4764-99cb-63a31a18fa44


### Was this PR authored or co-authored using generative AI tooling?

Yes. Generated-by: Claude Code (Anthropic). Co-authored with Claude,
reviewed line by line by the author before submission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… hides, and add a parameter to the card's operator

The ui-udf-parameters widget hides formly's labels and draws fixed
column headers (Value / Name / Type), so the author's per-sub-field
overrides from the Form View changed nothing a reader saw: a renamed
column kept its old name and a hidden one kept its header over an empty
cell. On a Form View card the widget now decides which columns are in
view from the first row's sub-fields (a hidden column, or one whose group
is hidden, is left out, header and cells alike), read live, since the
form's hide lands on a row after formly has built it; the header shows
the author's name for the column, else the panel's fixed label.

The Form View names the operator on every field it renders
(props.operatorID, FIELD_OPERATOR_ID_PROP / fieldOperatorID), since
nothing is highlighted on the form; the same mark tells a shared widget
which host it is on. Add parameter uses it: it inserts a declaration
into an operator's code, and it took that operator from the canvas
highlight, so on the form it edited the wrong operator or failed. It
now adds to the field's operator on a card, and to the highlighted one
on the panel. The declared parameters then reach the operator: the
panel's frame writes them only for the operator it shows while it acts
as the editor, so the form writes them when the frame does not (a writer
only, the operator still on the graph).

A reader's value cells stay locked: the Form View marks a reader's
field props.disabled, and the widget's own "the value is editable" used
to re-enable the control after formly had disabled it. And the widget's
own lock on the Name and Type cells (inferred from the code; a name
typed there no longer matches a declaration) now holds under any host:
the panel enables its whole form group when it turns interactive, and
formly mirrors a control's state into props.disabled, so a one-time
disable did not survive on the canvas while the form showed the cells
locked. The widget holds a locked cell through the control's
disabled-change hook, which disables it again whenever a host enables
it; a cell configured editable is released.

Closes apache#8763

Co-Authored-By: Claude Fable 5.1 <[email protected]>
…der, once

While authoring, the form's label wrapper decorates every row's sub-fields:
the first row with the name box and the eye, every later row with a static
name. The ui-udf-parameters renderer draws those sub-fields in its cells
under a header of its own, which this PR already makes read the author's
name -- so the name was drawn three times over: the header, the first row's
box, each later row's static label (and the static one said "Attribute
Type" where the header said "Type").

The wrapper gains one prop, labelHosted: a host that draws the label itself
sets it on the field, and the wrapper then renders no box, eye or static
label, only the control's accessible name and the dimmed state. The
renderer hosts the one name box and eye per column in its header, bound
to the first row's props and callbacks -- the row the form gave the
controls to -- and, for each column it hosts, sets the prop on every cell
of that column, with the column's own label as the accessible name so a
screen reader hears what the heading says. The write-through those controls make (the props first, then
the form) and the box's and eye's styles now have one definition each, on
the wrapper (rename / setHidden, _editable-label.scss), which the header
uses rather than copies.

Nothing in the form's walk changes; a renderer that draws no header of its
own (the generic array widget) sets nothing and is untouched; the operator
property panel is untouched too (no operator mark, no hosting). How a
later row's own name keeps up with a rename is the form's followersByPath,
as before this commit.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@yangzhang75
yangzhang75 force-pushed the fix/form-udf-parameters-overrides branch from b6a6c46 to 9e26b4f Compare November 12, 2026 20:51
@mengw15
mengw15 requested a max review from Copilot November 12, 2026 21:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues

Review effort: Lite

…d say where the lock stops meaning a run

Two from Copilot's review. subFieldViews takes a map; its guard let an
array through to the cast, so it now refuses one as it refuses a string.
And isExecutionInFlight's note said "there" for the Form View, which read
as a typo; it names the page now.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@mengw15

mengw15 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

/request-review @carloea2

kz930 pushed a commit to kz930/texera that referenced this pull request Nov 12, 2026
…eady holds (apache#9408)

### What changes were proposed in this PR?

Closes apache#9400.

A workflow's two views share one selection: it lives in the
root-provided `JointGraphWrapper`, which the switch between the canvas
and the Form View hands over rather than rebuilds. Nothing is lost in
the switch, but everything that shows the selection followed only the
highlight streams and never read it when it mounted. The event that made
the selection had already happened, so the view arriving at it waited
for an event that never came: the panel showed nothing, and the step was
drawn as not selected. Clicking that step did not help, since the
wrapper treats an already-highlighted element as a no-op and emits
nothing; it took clicking a *different* step to bring the panel back.

Each of the three now reads the selection it mounts into, then follows
the changes as before:

- `PropertyEditorComponent` (the canvas's panel and every host of it, so
not specific to the Form View, to any operator type, or to operators as
opposed to ports): the body of the highlight subscription becomes
`showHighlighted()`, the same decision as before (one operator, one
port, or nothing) with the `syncTexeraGraph` guard that the stream
applied as a `filter` now inside it, so it is applied once on both
paths. `registerHighlightEventsHandler()` calls it before subscribing,
and only when something is highlighted: the no-frame branch is for a
selection being dropped (it clears the editing marker on the shared
awareness channel and resizes the docked panel to its empty content),
which a fresh mount must not do. It is also called after the toolbar's
choosing toggle has replayed its value, so the frame it builds carries
`choosing` from the start instead of being torn down and rebuilt by the
replay inside the page's first change-detection pass. Not `startWith` on
the stream: its initial emission is unconditional, which is exactly the
empty-branch-at-mount this avoids.
- `WorkflowEditorComponent`: the ring. The paper is new on every switch,
and the highlighter was applied only from the stream, so the arriving
view would have shown its panel for a step drawn as not selected. The
handler now rings the operators and comment boxes the wrapper already
holds, then follows the streams; the `findViewByModel` guard the
unhighlight branch already had is shared with it.
- `WorkflowFormComponent`: one call to the
`syncSelectionFromHighlight()` it already has, in `ngOnInit` after it
subscribes, so the form's step panel opens on the selection it arrives
with. The template note that justified `[hidden]` over `*ngIf` by the
panel having "no initial pull" is reworded to the reason that still
holds.

No change to who owns the selection, to what a click does, or to the
canvas's own behaviour when it is the only view open: with nothing
selected, nothing is read, and both panels mount empty exactly as
before. The wrapper's no-op for an already-highlighted element is
deliberately untouched (it is what keeps a shift-selection stable); it
simply stops mattering, because the arriving view already shows that
step.

#### Video

**Before**



https://github.com/user-attachments/assets/e00affd4-46aa-4691-acc4-5d33060da766


**After**




https://github.com/user-attachments/assets/f50674e4-b0fd-4941-bcca-860d1aa61abe


### Any related issues, documentation, discussions?

Closes apache#9400. Part of the Form View (apache#8011).

Related to apache#8846, which touches `workflow-form.component.ts` elsewhere
and does not read the selection on mount: its `frameWrites` guard
compares `selectedOperatorId`, which only `syncSelectionFromHighlight()`
sets, so on arriving in the Form View with a step selected that guard
saw `undefined` and the form wrote the declared parameters even while
the live panel showed that step. With this change `selectedOperatorId`
is set on arrival and the guard holds. The two do not overlap in code;
whichever lands second rebases.

### How was this PR tested?

- `property-editor.component.spec.ts`: a panel mounting into an existing
selection shows that operator; mounting into no selection leaves the
panel alone and mounting into two shows no frame (the two-operator
selection is made explicit with `setMultiSelectMode(true)` and asserted
before the check); and the frame a panel mounts into is built with the
toolbar's choosing state, with no rebuild, which pins the order of the
two subscriptions.
- `workflow-editor.component.spec.ts`: wiring the highlight handler into
a selection the wrapper already holds rings that operator once (a spy
installed after the stream has done its work).
- `workflow-form.component.spec.ts`: the form's step panel opens on the
step the canvas had selected, and stays closed for none or several —
seeded with a stale id, so a read that never happened could not pass as
"undefined".
- Existing specs for all three components pass unchanged; they cover
what the streams do after mount.
- The full frontend suite, eslint, prettier and the production (AOT)
build pass.
- By hand on a local deployment, `main` (`cfca8e1e5`) and this branch
served side by side on the same backend and the same workflow, driven by
a Playwright probe that reads what the panel shows at each step:

| step | main | this branch |
| --- | --- | --- |
| click the step on the canvas | the step | the step |
| switch to the Form View | empty | the step, ringed on the preview |
| click that same step on the preview | empty | the step |
| switch back to the canvas | empty | the step, ringed on the canvas |

### Was this PR authored or co-authored using generative AI tooling?

Yes. Generated-by: Claude Code (Claude Fable 5.1, Anthropic).
Co-authored with Claude, reviewed line by line by the author before
submission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
…p rebuild in fewer cases

The widget's spec pinned the rule for Add parameter on a card (a writer's
offers it without the canvas's flag, a reader's does not, nor a writer's
mid-run) six times by hand, and the formly spec pins the same rule under
real formly, where a reader's lock is the one formly applied -- the only
place the rule can be told from its mock. The hand-made six become two:
what the widget does with the rule (adds to the card's operator; adds
nothing and keeps the draft open when the card is not editable, whoever
calls). The form's spec folds the catch-up rebuild's twelve cases into
nine: a rebuild that cuts the old cards off and one that holds the new
rows are one rebuild, the frame being the writer and the form leaving
the write to it are one case, and no card showing the step is one case
whether the input is another or the step is. Every line and branch the
PR adds stays covered.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

@mengw15 mengw15 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. @carloea2, since you wrote the UI parameter table, could you also take a look, mainly at how Add parameter works on a Form View card?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix frontend Changes related to the frontend GUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(workflow-form): ui-udf-parameters ignores the binding's rename and hide overrides

5 participants