They tell an admin which placeholders a template accepts and then leave them to retype {{holdDuration}} by hand into the subject or the body, getting the braces and the exact spelling right unaided. A typo does not announce itself either — a misspelled placeholder is not a required one, so the save succeeds and the email ships with a literal {{holdDuraton}} in it.
Clicking a chip should insert its tag at the current cursor position in whichever field was last focused, the subject line or the body.
This matters more since #136 than it did before. verification and passwordReset went from two placeholders to five, cartReminder from three to six, and favoriteSold and favoriteWithdrawn from two to five. The list is now long enough to be worth clicking rather than reading.
What makes this more than a click handler
There are two targets with different mechanics. The subject is a plain antd Input; the body is MDEditor from @uiw/react-md-editor, which owns its own textarea. Inserting means tracking which of the two was focused most recently, because a click on a chip moves focus away from both and the component then has to know where to put the text.
Cursor position has to survive the insert. Both fields are controlled — subject and body are React state — so the insert is a string splice plus a setState, after which React re-renders and the caret goes to the end of the field unless it is explicitly restored. It should land immediately after the inserted tag so an admin can keep typing. That means holding a ref to each underlying textarea/input and setting selectionStart/selectionEnd after the state has committed.
Mutating the DOM directly will appear to work and will not. Writing into the textarea's value does not update body, so the next keystroke re-renders from state and the inserted placeholder vanishes. The insert has to go through the same onChange path typing does.
A selection should be replaced, not straddled. If text is selected when the chip is clicked, the tag should replace it, which is what every editor does.
The chips need to become real controls. An antd Tag is a span, so as it stands a keyboard user cannot reach or activate one. Clicking is only half the feature; these should be buttons, reachable by tab and activated by enter and space, and still look like the compact monospace chips they are today.
There has to be a defined behaviour before either field is focused. Landing on the tab and clicking a chip straight away should do something predictable rather than nothing — appending to the body is the obvious default, but it should be a decision rather than an accident of which ref happens to be set.
Acceptance criteria
Clicking a placeholder chip inserts its {{tag}} at the caret in whichever of the subject or body was last focused
A selection in the target field is replaced by the tag
The caret ends up directly after the inserted tag, and the field keeps focus so typing continues naturally
The inserted text goes through the normal change path, so the preview updates and the value persists on save
Chips are keyboard reachable and activate on enter and space
Behaviour before either field has been focused is defined and predictable
Covered by an e2e test that inserts into both the subject and the body, mid-text rather than at the end
The row of placeholder chips above each email editor is display-only. In `frontend/src/admin/EmailTemplateEditor.tsx` they are plain antd `Tag`s:
```tsx
{template.available.map((name) => (
<Tag key={name} style={{ fontFamily: 'monospace' }}>{`{{${name}}}`}</Tag>
))}
```
They tell an admin which placeholders a template accepts and then leave them to retype `{{holdDuration}}` by hand into the subject or the body, getting the braces and the exact spelling right unaided. A typo does not announce itself either — a misspelled placeholder is not a required one, so the save succeeds and the email ships with a literal `{{holdDuraton}}` in it.
Clicking a chip should insert its tag at the current cursor position in whichever field was last focused, the subject line or the body.
This matters more since #136 than it did before. `verification` and `passwordReset` went from two placeholders to five, `cartReminder` from three to six, and `favoriteSold` and `favoriteWithdrawn` from two to five. The list is now long enough to be worth clicking rather than reading.
### What makes this more than a click handler
**There are two targets with different mechanics.** The subject is a plain antd `Input`; the body is `MDEditor` from `@uiw/react-md-editor`, which owns its own textarea. Inserting means tracking which of the two was focused most recently, because a click on a chip moves focus away from both and the component then has to know where to put the text.
**Cursor position has to survive the insert.** Both fields are controlled — `subject` and `body` are React state — so the insert is a string splice plus a `setState`, after which React re-renders and the caret goes to the end of the field unless it is explicitly restored. It should land immediately after the inserted tag so an admin can keep typing. That means holding a ref to each underlying textarea/input and setting `selectionStart`/`selectionEnd` after the state has committed.
**Mutating the DOM directly will appear to work and will not.** Writing into the textarea's `value` does not update `body`, so the next keystroke re-renders from state and the inserted placeholder vanishes. The insert has to go through the same `onChange` path typing does.
**A selection should be replaced, not straddled.** If text is selected when the chip is clicked, the tag should replace it, which is what every editor does.
**The chips need to become real controls.** An antd `Tag` is a `span`, so as it stands a keyboard user cannot reach or activate one. Clicking is only half the feature; these should be buttons, reachable by tab and activated by enter and space, and still look like the compact monospace chips they are today.
**There has to be a defined behaviour before either field is focused.** Landing on the tab and clicking a chip straight away should do something predictable rather than nothing — appending to the body is the obvious default, but it should be a decision rather than an accident of which ref happens to be set.
### Acceptance criteria
- Clicking a placeholder chip inserts its `{{tag}}` at the caret in whichever of the subject or body was last focused
- A selection in the target field is replaced by the tag
- The caret ends up directly after the inserted tag, and the field keeps focus so typing continues naturally
- The inserted text goes through the normal change path, so the preview updates and the value persists on save
- Chips are keyboard reachable and activate on enter and space
- Behaviour before either field has been focused is defined and predictable
- Covered by an e2e test that inserts into both the subject and the body, mid-text rather than at the end
bermudalamb
added this to the Customer and Admin UI review findings project 2026-08-23 09:11:24 -05:00
bermudalamb
self-assigned this 2026-08-23 09:11:30 -05:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The row of placeholder chips above each email editor is display-only. In
frontend/src/admin/EmailTemplateEditor.tsxthey are plain antdTags:They tell an admin which placeholders a template accepts and then leave them to retype
{{holdDuration}}by hand into the subject or the body, getting the braces and the exact spelling right unaided. A typo does not announce itself either — a misspelled placeholder is not a required one, so the save succeeds and the email ships with a literal{{holdDuraton}}in it.Clicking a chip should insert its tag at the current cursor position in whichever field was last focused, the subject line or the body.
This matters more since #136 than it did before.
verificationandpasswordResetwent from two placeholders to five,cartReminderfrom three to six, andfavoriteSoldandfavoriteWithdrawnfrom two to five. The list is now long enough to be worth clicking rather than reading.What makes this more than a click handler
There are two targets with different mechanics. The subject is a plain antd
Input; the body isMDEditorfrom@uiw/react-md-editor, which owns its own textarea. Inserting means tracking which of the two was focused most recently, because a click on a chip moves focus away from both and the component then has to know where to put the text.Cursor position has to survive the insert. Both fields are controlled —
subjectandbodyare React state — so the insert is a string splice plus asetState, after which React re-renders and the caret goes to the end of the field unless it is explicitly restored. It should land immediately after the inserted tag so an admin can keep typing. That means holding a ref to each underlying textarea/input and settingselectionStart/selectionEndafter the state has committed.Mutating the DOM directly will appear to work and will not. Writing into the textarea's
valuedoes not updatebody, so the next keystroke re-renders from state and the inserted placeholder vanishes. The insert has to go through the sameonChangepath typing does.A selection should be replaced, not straddled. If text is selected when the chip is clicked, the tag should replace it, which is what every editor does.
The chips need to become real controls. An antd
Tagis aspan, so as it stands a keyboard user cannot reach or activate one. Clicking is only half the feature; these should be buttons, reachable by tab and activated by enter and space, and still look like the compact monospace chips they are today.There has to be a defined behaviour before either field is focused. Landing on the tab and clicking a chip straight away should do something predictable rather than nothing — appending to the body is the obvious default, but it should be a decision rather than an accident of which ref happens to be set.
Acceptance criteria
{{tag}}at the caret in whichever of the subject or body was last focused