Skip to content

fix: show the textarea the saved notifier configuration actually uses - #406

Merged
BobDu merged 1 commit into
jenkinsci:mainfrom
BobDu:fix/369-raw-textarea-toggle
Jul 30, 2026
Merged

fix: show the textarea the saved notifier configuration actually uses#406
BobDu merged 1 commit into
jenkinsci:mainfrom
BobDu:fix/369-raw-textarea-toggle

Conversation

@BobDu

@BobDu BobDu commented Jul 30, 2026

Copy link
Copy Markdown
Member

Fixes #369

A notifier has two textareas that both take message content, and which one is sent depends on the "disable built-in message" checkbox: unchecked sends the built-in message with 自定义内容 appended, checked sends 自定义消息 and nothing else. The form is supposed to show whichever one applies and hide the other. It showed the wrong one, in two different ways.

The reporter's log says it all

{"raw":true, "content":"## <font color=green>APP意见反馈接口测试</font> ...", "message":""}

Their markdown went into 自定义内容, but raw: true sends 自定义消息, which was empty — so the request went out with a blank markdown text and DingTalk answered 400403 参数 markdown --》 text 缺失.

They had not misconfigured anything. The form offered them the textarea that mode does not send.

Two defects

The saved value was never read back. The view hardcodes display: none on the custom block and the script only assigned styles from onchange, so every render offered 自定义内容 and hid 自定义消息 however the notifier had been saved. Reopening a notifier saved with the built-in message disabled therefore presented the field it no longer sends, and edits made there landed where nothing reads them.

The handler addressed the wrong notifier. It reached for its blocks with document.querySelector, which returns the first match in the document rather than the one belonging to the checkbox that changed. Every configured robot renders this same markup, so with more than one robot the second robot's checkbox toggled the first robot's textareas and left its own alone — the checkbox did nothing observable at all.

How it got here

#239 reported the visible half of this in 2024 and was closed by #240, which replaced window.addEventListener('load') plus a single querySelector with Behaviour.specify so that every checkbox gets a handler. That part was right, but both document.querySelector calls moved into the new callback unchanged, and the initial render was never part of it.

Before either of them, the de-jQuery-ification in 70215a4 had translated $('.x').css(…), which applies to every match, into document.querySelector('.x').style, which applies to the first. That is where addressing the wrong notifier came from — with a single robot configured, as in development, all three versions look identical, which is why it survived three years.

The fix

Behaviour.specify invokes its callback once per matching element as the form renders, which is exactly where the saved state has to be applied, so the same function now serves both the first paint and later changes. The blocks are resolved with closest('.repeated-chunk') — the container core itself reaches for the same way, in hetero-list.js and in the help-area lookup in hudson-behavior.js.

Why the surrounding markup moves too

This view renders the per-item structure of lib/form/hetero-list itself rather than using the tag, and the fix depends on that structure, so it is worth saying why: its items are one per robot, all sharing one descriptor. The tag builds one prototype per descriptor and oneEach caps the list at that count, so through the tag a job could configure exactly one robot, listed under the descriptor's class name rather than the robot's.

That hand-rolled copy had drifted, missing the header and content structure core has since grown, so the notifier name sat as loose text where every other list on the page shows a styled header. Both are ported here. The drag handle deliberately is not: the order comes from the global robot list, so it is not the job's to rearrange.

before after
chunk header none; robot name as plain text inside the body repeated-chunk__header--no-handle, naming the robot
chunk body direct children of .repeated-chunk wrapped in jenkins-repeated-chunk__content

The empty f:entry that used to carry the robot name goes with it, which is also how core writes this — all eight f:advanced usages in core sit beside f:entry, never inside one, and the tag already emits its own .jenkins-form-item.

Tests

Four cases, all against a real job configuration page with two robots.

test what it pins
offersTheBlockEachSavedNotifierActuallySends each saved notifier is offered the block it actually sends
togglingOneNotifierLeavesTheOtherAlone ticking the second notifier moves its own blocks and leaves the first one's alone
namesEachRobotInItsOwnChunkHeader each chunk's header names its robot, and the body is wrapped the way core's tag wraps it
savingTheFormUnchangedKeepsEveryNotifier saving unchanged returns every notifier with its content, message, mentions, mode, enabled state and notice occasions

The first two fail against the previous script. The second one ticks rather than unticks deliberately: unticking moves a notifier towards the markup's own default, so looking the block up document-wide lands on the same result and the test passes while the bug is present. Removing the initial call fails only the first case and dropping the onchange assignment fails only the second, so neither case stands in for the other.

The round-trip case seeds the two notifiers with different values in every field, because getNoticeOccasions() falls back to the global default — a set that went missing would come back as all six occasions rather than as null, and identical seeds would hide that.

One path is not covered: a notifier added through the "add robot" menu takes the same route, since the hetero-list script applies behaviours to the subtree it inserts, but that menu is a dropdown the test client cannot open. That half was checked by hand and it is noted in the test's javadoc.

Not addressed here

There is extra space under the Advanced button in these chunks, and it changes after the section is expanded and collapsed. That is core's: jenkins-repeated-chunk__content > *:last-of-type { margin-bottom: 0 } keys the trailing margin on :last-of-type, which counts display: none siblings of the same tag, and advanced.js permanently moves the advanced rows to sit after that block. Core's own Execute shell chunk behaves identically. Working around it in this plugin would only pin our markup order to a core selector that is likely to change, so it is left alone.

A notifier has two textareas that both take message content, and which one is sent
depends on the "disable built-in message" checkbox: unchecked sends the built-in
message with `自定义内容` appended, checked sends `自定义消息` and nothing else. The
form is supposed to show whichever one applies and hide the other. It showed the
wrong one, in two different ways.

The markup hardcodes `display: none` on the custom block and nothing read the saved
value back, while the script only assigned styles from `onchange`. So every render
offered `自定义内容` and hid `自定义消息`, however the notifier had been saved. A
notifier saved with the built-in message disabled therefore came back offering the
textarea it no longer sends, and edits made there landed in a field nothing reads —
`message` stayed empty, the request went out with a blank markdown text, and DingTalk
answered `400403 参数 markdown --》 text 缺失`. That is jenkinsci#369.

The handler also reached for its blocks with `document.querySelector`, which returns
the first match in the document rather than the one belonging to the checkbox that
changed. Every configured robot renders this same markup, so with more than one robot
the second robot's checkbox toggled the first robot's textareas and left its own
alone — the checkbox did nothing observable at all.

jenkinsci#239 reported the visible half of this in 2024 and was closed by jenkinsci#240, which replaced
`window.addEventListener('load')` plus a single `querySelector` with
`Behaviour.specify` so that every checkbox gets a handler. That part was right, but
both `document.querySelector` calls moved into the new callback unchanged, and the
initial render was never part of it. Before either of them, the de-jQuery-ification in
`70215a4` had translated `$('.x').css(...)`, which applies to every match, into
`document.querySelector('.x').style`, which applies to the first — that is where
addressing the wrong notifier came from.

`Behaviour.specify` invokes its callback once per matching element as the form
renders, which is exactly where the saved state has to be applied, so the same
function now serves both the first paint and later changes. The blocks are resolved
with `closest('.repeated-chunk')`, the container core itself reaches for the same way.

Which is worth being explicit about, because this view renders the per-item structure
of `lib/form/hetero-list` itself rather than using the tag: its items are one per
robot, all sharing one descriptor, and the tag builds one prototype per descriptor and
caps a `oneEach` list at that many items — so through the tag a job could configure
exactly one robot, listed under the descriptor's class name. That copy had drifted,
missing the header and content structure core has since grown, so the notifier name
was loose text where every other list on the page shows a styled header. It is ported
here, without the drag handle: the order comes from the global robot list, so it is not
the job's to rearrange.

Two tests cover the form. `DingTalkNotifierConfigFormTest` renders a real job
configuration page with two robots and asserts that each saved notifier is offered the
block it actually sends, and that ticking the second one moves its own blocks and
leaves the first one's alone; both fail against the previous script. The second case
ticks rather than unticks deliberately — unticking moves a notifier towards the
markup's own default, so looking the block up document-wide lands on the same result
and the test passes while the bug is present. Removing the initial call fails only the
first case and dropping the `onchange` assignment fails only the second, so neither
case stands in for the other. `DingTalkJobPropertyFormTest` covers the markup this
view owns: that each chunk's header names its robot, and that saving the page unchanged
gives back every notifier with its content, message, mentions and mode intact.

Signed-off-by: BobDu <i@bobdu.cc>
@BobDu
BobDu merged commit 9d7b1cb into jenkinsci:main Jul 30, 2026
16 checks passed
@BobDu
BobDu deleted the fix/369-raw-textarea-toggle branch July 30, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

机器人发送自定义消息失败

1 participant