Repository navigation
fix(leads,contacts): the Convert Lead screen names the account the lead will join; Add to Campaign on a contact is declared on the row menu only (#2073 items 5b, 8) - #2203
Merged
objectstack-fleet[bot] merged 4 commits intoOct 20, 2026
Conversation
…ad will join; Add to Campaign on a contact declares the row menu only The conversion's account match now runs ahead of the screen, and the screen's body text says, before the rep submits, whether the lead joins an existing account (named) or creates a new one (named after the company). The submit branches on that same read. Every locale pack translates the notice. The contact action drops its list_toolbar placement, as the lead action did. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…he conversion notice and the contact action The leads page (three locales) says the form shows which account the lead will join, that the match only looks at accounts the rep can see, and what happens when a colleague's unshared account carries the same name. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…, Convert Lead Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 20, 2026
Contributor
Author
席位核对ACCEPT (#2073 items 5b, 8) · PR #2203 at
Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing |
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2073 (items 5b, 8)
Clause-②: no
Items 5b and 8 only. The card stays open for item 6 (its second half).
What this does
Item 8, the Convert Lead screen. It asked only Create Opportunity? (是否创建商机?), and
find_accountmatched an account by the lead's normalized company name after the screen. The rep was never told which account the lead would join.src/sales/flows/lead-conversion.flow.ts:find_accountruns ahead ofscreen_1. This is the patternget_leadalready follows: a read before the screen, every write behind it. A new assignment node,compose_account_notice, binds exactly one ofexistingAccountName(the matched account's ownname) andnewAccountName(the lead's company) from that read, and sets the other tonull. The screen's body text now carries an account notice after the duplicate warning:decision_account, unchanged), so what the screen says is what is done. No match runs after the screen.e31(decision_match_key → screen_1),e44(decision_amount → find_account) ande4(find_account → decision_account) are retired and left vacant.e48carriese31's condition ontofind_account,e49ande50lead through the notice to the screen, ande51carriese44's condition ontodecision_account.descriptionis a per-run{{ }}text slot, which the engine translates in the run's locale. A hole is a path plus an optional formatter, with no logic (@objectstack/formula's template engine), anddefault:is its only conditional. So each hole'sdefault:carries the other branch's words, and the sentence is built name-first. Composing the sentence in a variable would ship it in English to every locale, because no pack reaches a variable's text.src/sales/translations/{zh-CN,es-ES,ja-JP}/app.ts:flows.lead_conversion.screens.screen_1.descriptiontranslates the whole template. Theenpack carries noflowsblock; the authored copy is English.opportunityName's default ([Decision] Which of the 33 UI and UX suggestions from the 2026-10-20 browser passes to adopt (seat: adopt 23, fold 4 into open cards, defer 6) #2194's T1-U09 is undecided), every field, every refusal.Item 5b, Add to Campaign on a contact.
src/sales/actions/contact.actions.ts:add_contact_to_campaignnow declareslocations: ['list_item'], the same ascreate_campaignafter fix(leads,reports): Add to Campaign leaves the lead list header; the funnel report charts amount by stage as bars (#2073 items 5, 10) #2198. A ⛔ comment beside it says whylist_toolbarmust not come back.visiblereadsrecord.email_opt_outand a list header binds no record. All Contacts still opens the selection bar throughbulkActions.Tests.
test/lead-conversion-account-notice.test.ts, 13 cases:find_accountand the notice precedescreen_1, no node ahead of the screen writes, and no match runs after it.templateEnginefor both branches with the same company name. The pin is that existing and new read differently, each names the account exactly once, and the duplicate warning still leads.sales_rep, each reading back the account it ends on: an account the rep owns; no match; the named account deleted before the submit; a colleague's unshared account with the same name; and one differing only in case and spacing.test/campaign-enrollment-doors.test.ts: a describe for 5b. It checks the declaration, and that All Contacts still names the action inbulkActionsin the per-record form.test/account-name-normalized-match.test.ts: the match-key partition readse32/e48.test/lead-duplicate-visibility.test.ts: two "no warning on a clean lead" cases now assert the warning's absence instead ofdescription === undefined. The body text always carries the notice now.Docs,
content/docs/sales/leadsin three locales, conversion section only:Changeset.
.changeset/2073-conversion-screen-names-the-account.md(patch).Item 8: show the match, or let the rep pick, by the four axes
What the platform can express was measured first.
Show needs the match read before the screen, plus a translated template; both exist.
Pick would be a
type: 'lookup'screen field withreference: 'crm_account', defaulting to the match. The console's runner draws that as a record picker over the user's own data source, so it offers only accounts the rep can see.Real business need. Measured on the shipped demo: 3 of the 21 seeded leads carry a company equal to a seeded account's name, and 0 carry a near-miss that normalization misses and a person would have to pick by hand. The card's tester saw only the missing information ("Matching into the rep's own account worked"). The existing correction path for a mismatch is the lead's own Company, the match key's producer. Showing the match makes that path visible: a rep who reads "creates a new account named Acme Corp" when the customer is Acme Corporation can cancel and fix it. Picking adds re-parenting a conversion onto any account, and nothing measured asks for that. The picker would not reach a hidden account either, so it does not help the hidden-account paths.
Long-term soundness. Show keeps the normalized key as the one authority: the producer canonicalizes, the flow matches, the screen reports. Pick adds a second authority, an id the client posts. The flow would have to re-read it as the user, and it would have to give "cleared the prefilled match" a meaning. That meaning is a same-name create, which is either the unique-name refusal or a case-variant duplicate.
Making AI mistakes harder. Show is one read moved, one assignment and one template, and nothing downstream has to tolerate anything. Pick puts a client-collected id into writes, a trust boundary that is easy to forget to re-check.
No scope growth. Show adds no capability. Pick adds one.
So the choice is show.
Measured
Engine:
@objectstack/verifyboot of this app on 18.0.0-next.5. The converter is asales_repand the colleague is anothersales_rep;crm_accountissharingModel: 'private', so neither reads the other's accounts. The screen is submitted with its prefilled bag and the box unticked. Before ismain9db9a58b; after is this branch's flow, unchanged sincee88a7c26.FLOW_FAILED400 "Node 'create_account' failed: … Duplicate record refused on 'crm_account' … No record was written.", lead unconvertedFLOW_FAILED400 atcreate_contact("requires edit access to its master record"), nothing written, lead unconvertedBrowser:
objectstack devon this worktree withpnpm demo:staff, Chromium at/opt/pw-browsers/chromium, 1440×900, signed in aseu.rep@objectos.ai(sales_rep+eu_sales_team, who sees 2 accounts: Lattice Education and Northwind Energy). Each path ran from the lead's Convert Lead button, then Submit with the box unticked, then a read back as the admin. The screenshots are kept on the rig.9db9a58b), en-US: the dialog read "Lead Conversion Process / Conversion Details / Create Opportunity?" on all four paths. zh-CN read "线索转化流程 / 转化详情 / 是否创建商机?".VERTEX analytics) createdVERTEX analyticsbesideVertex Analytics.tEjAHnzxbV8-6gR4, Lattice Education.STARK medical): the new-account sentence naming STARK medical. It createdSTARK medical, owned by the rep, beside Stark Medical.Mechanism assumptions
get_leadran ahead ofscreen_1(e0,e27, and the gates toe31).find_accountran after it (e43→decision_amount→e44) and only read.use_new_account/use_existing_accountnormalize both branches ontoaccountId.descriptionis a{{ }}text slot rendered per run and translated by the engine (renderFlowTextSlot, keyflows.lead_conversion.screens.screen_1.description).defaultValueis a CEL envelope evaluated before the wire.inlineHelpTextis forwarded raw, so it is static and cannot show a fetched name.@objectstack/console18.0.0-next.5) drawstype: 'lookup'with areferenceas a record picker over the user's data source.create_contactrefusing an account the rep cannot edit ("requires edit access to its master record"), so the write as the user would refuse it. That is inferred for a picked id.flows.lead_conversion.screens.screen_1block translates the screen, anddescriptionalready existed there as{{duplicateWarning}}. All three non-en packs carry the new template.pnpm lint:i18n-gatereads 0i18n/missing-*.content/docs/sales/leads.mdxand its twins describe the form ("A small form pops up asking …"). The conversion section is updated.Tests and gates (head 541524d,
mainb91ac1b merged)pnpm verify, step by step through the shared verify lock. Each exit is read from the lock'sVERDICT command-exitline.pnpm validate && pnpm typecheck && pnpm lint && pnpm lint:i18n-gate && pnpm hygiene && pnpm hygiene:tokens && pnpm buildi18n/missing-*issues"; "source hygiene clean"; src/sales business semantics ~57,643 tokens (ceiling ~88,500); build complete with the same 8 author-time warnings, none naming these itemsvitest run --shard=1/3 --maxWorkers=2vitest run --shard=2/3 --maxWorkers=2it.skipIfintest/script-main-guard.test.ts)vitest run --shard=3/3 --maxWorkers=2find test -name '*.test.ts'gives.tsc --listFileslists both changed test files, sotypecheckcovers them.Ablation, one per changed behaviour, run once and not kept. Both ran on
ed681098throughscripts/ablation-replace.mjsin wrap mode, inside a script trappinggit checkout HEAD --on both absolute paths. No build is needed: the tests readsrc/through the composed stack.list_toolbarback onadd_contact_to_campaign. The anchor went 1 → 0 and the blob3617682b→ac02b5ab. The 5b describe gave "Tests 1 failed | 5 passed | 28 skipped (34)", the predicted case: "declares the row menu as its only location".'{{duplicateWarning}}'. The anchor went 1 → 0 and the blobace246ec→fc4b766c.test/lead-conversion-account-notice.test.tsgave "Tests 7 failed | 6 passed (13)", the 7 predicted: the en render case, the warning-ahead case, and the five runtime paths. The three pack render cases and the structural cases stayed green.HEAD:again, andgit diff HEADis empty.Acceptance notes
docs/feature-inventory.mdstays with Browser pass #2058 — HotCRM-side defects (18 items: accepted quote still takes lines, half-converted leads, agents losing their cases, unreachable Sales Home, …) — list card, split per item at dispatch #2060 item 16, and no row there becomes wrong. LEA-010 describes the dedupe, which is unchanged; the account notice is not in it, which is that claim's call.getting-started/quick-tour(three locales) also calls the button Convert. That page is outside this claim, and it is reported to the seat.Generated by Claude Code