Skip to content

feat(bpm-core-react): surface approval comments and allow multi-person ad-hoc targets - #36

Open
coollugi wants to merge 4 commits into
Rytass:mainfrom
coollugi:feat/adhoc-target-and-history-comment
Open

feat(bpm-core-react): surface approval comments and allow multi-person ad-hoc targets#36
coollugi wants to merge 4 commits into
Rytass:mainfrom
coollugi:feat/adhoc-target-and-history-comment

Conversation

@coollugi

@coollugi coollugi commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Two independent UI changes, one commit each, both from the same customer UAT
round as #35. They touch the instance detail view only — no engine, schema or
GraphQL change.

1. An approval comment gets its own line (2b03494)

Reported as "簽核意見不明顯,跟簽核歷程的其他資訊混在一起,且字很小".

Every part of a timeline entry was appended to one variant="caption" run
joined by " · ", so what a person wrote sat between the node label, the
actor, the timestamp and the signature hash:

節點:簽核節點 1 · 操作者:陳財務經理 · 時間:… · 決議:同意 ·
同意說明:測試 · 簽章:已驗證(5f412a0ccbe2…)

ActivityStepDescriptionPart gains a comment member; the four decision
comments (approve / reject / return / transfer) use it, and the step renders
metadata inline as before with each comment on its own line at body size.
whiteSpace: pre-wrap keeps line breaks the writer typed, which the joined run
flattened.

renderActivityDescriptionPart now takes Exclude<…, { type: 'comment' }> so
"comments never go inline" is a type error rather than a convention.

2. Ad-hoc targets accept several people (4543bee)

AdhocTargetInput.memberIds has always been a list, but the picker was
mode="single" and the handler sent [selectedMember.id], so adding three
countersigners meant opening the dialog three times.

Now mode="multiple" with overflowStrategy="wrap". Two things came along:

  • menuMaxHeight — the options are the member directory, which has no
    upper bound; without it the menu grows past the viewport with no scrollbar.
  • clearable starts working, since this component's single mode never
    wired it up.

onSearchTextChange is dropped from this picker only. It auto-selected when
the typed text matched exactly one option, which is a single-selection
convenience that would fight a list being built up. The transfer picker keeps
it and is untouched.

Tests

New sections/shared.spec.ts, 10 cases over two pure functions
(readActivityDetailParts, readAdhocTargetDraft). Both headline assertions
were checked against a deliberately broken implementation first — the comment
case fails when the comment is emitted as a text part, the multi-member case
fails when the draft slices to the first id.

Building the target moved into readAdhocTargetDraft() so that decision is
testable without rendering the dialog; it is the part whose shape changed.

Full suite green (pnpm test, 6 projects), pnpm typecheck clean, pnpm lint
0 errors — the 5 warnings are pre-existing and in other files.

What I did not do, and why it needs your call

Targeting a department or a position from the ad-hoc dialog. The customer
asked to pick countersigners by employee number or department. The engine is
ready — AdhocTargetKindEnum already has ORG_UNIT_MEMBER (with
includeDescendants) and POSITION, and OrgUnitPicker / PositionPicker
already exist in components/admin-pickers.tsx.

The blocker is data access: both pickers take a pre-fetched list, and the only
query that returns org units and positions is organizationDashboard, whose
resolver is @BPMAdminOnly(). An ordinary approver cannot read it, so wiring
it into an approver-facing dialog would either fail for the people who need it
or require widening that resolver.

That looks like an API design decision rather than a UI one, so I stopped here
rather than inventing a query shape. Happy to follow whatever you prefer — a
narrow non-admin query for picker data, scoping the existing one, or something
else.

Searching members by employee number or department is the other half of the
same request and is equally not a UI change: searchMembers(searchText: String!)
is one free-text argument whose matching fields are decided by the host's
BPMMemberResolver.search.

One more thing you will know better than me: the member picker has
disabledOptionsFilter but not clearSearchText={false}. In a repo I work in,
that combination made mouse-selection after typing silently fail while keyboard
Enter worked. I left it alone because I could not reproduce it here and the
flag has its own downside, but it may matter more now that a person clicks
several options in a row.

🤖 Generated with Claude Code

https://claude.ai/code/session_018STtLMQTp1GVCYFp9GPSfg

coollugi and others added 4 commits September 7, 2026 18:33
…meline

Every part of a timeline entry was appended to one caption-sized run joined by
" · ", so what an approver wrote sat between the node label, the actor, the
timestamp and the signature hash:

    節點:簽核節點 1 · 操作者:陳財務經理 · 時間:… · 決議:同意 ·
    同意說明:測試 · 簽章:已驗證(5f412a0ccbe2…)

`ActivityStepDescriptionPart` gains a `comment` member, and the four decision
comments (approve / reject / return / transfer) now use it. The step renders
metadata inline as before and each comment on its own line at `body` size with
its label and a left rule, red for a rejection.

`whiteSpace: pre-wrap` keeps line breaks a person typed, which the joined run
also flattened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018STtLMQTp1GVCYFp9GPSfg
`AdhocTargetInput.memberIds` has always been a list, but the picker was
`mode="single"` and the handler sent `[selectedMember.id]`, so adding three
countersigners meant opening the dialog three times.

The picker is now `mode="multiple"` with `overflowStrategy="wrap"` so the
chosen names stay visible, and the selection maps straight onto `memberIds`.
Going multiple also picks up `clearable`, which this component's single mode
never wired up.

`menuMaxHeight` is set while the picker is open anyway: the list is the member
directory, which has no upper bound, and without it the menu grows past the
viewport with no scrollbar.

The "which target do we send" decision moved into `readAdhocTargetDraft()` in
`shared.ts` so it can be tested without rendering the dialog; that is the part
that changed shape here.

`onSearchTextChange` is dropped from this picker. It called
`readUniqueMemberOption` to auto-select when the typed text matched exactly one
option, which is a single-selection convenience — under multiple selection it
would fight the list the approver is building. The transfer picker still uses
it and is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018STtLMQTp1GVCYFp9GPSfg
…omment

Moving an approval comment onto its own line dropped two things the inline
`dangerText` part used to carry.

The label and the text were rendered adjacent with only a 4px margin between
them, so the timeline read "拒絕原因資料不足,請補件" — the margin is invisible
to `textContent`, and every other part of a step still reads "X:Y". The margin
goes with it: ":" already carries its own trailing half-em.

The danger tone only overrode `borderLeftColor`, so a rejection reason lost
`--mzn-color-text-error` and became indistinguishable from an approval note.
It now keeps that colour, label included.

Writing the border as longhands is defensive rather than a fix — the spread
already put the `borderLeftColor` override after the shorthand, so the result
was correct — but it removes the dependency on that ordering.

Also drops the inline `dangerText` part this replaced: the comment part left
it with no callers, so `readDangerTextDescriptionPart`, the variant itself,
`HISTORY_DANGER_TEXT_STYLE` and its renderer branch were all unreachable.

Adds a render spec for the separator and both tones, since the only cover they
had was e2e.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…xports it

`@rytass/bpm-core-client` has no root export for it, so the spec failed
`tsc -p tsconfig.spec.json` even though jest was green — a type-only import is
erased before ts-jest ever resolves it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants