Repository navigation
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
Merged
objectstack-fleet[bot] merged 2 commits intoOct 20, 2026
Conversation
…funnel report charts amount by stage as bars Add to Campaign on a lead (create_campaign) runs on exactly one record, but its list_toolbar location drew it in the header of every lead view that names it in no bulkActions: seven views, measured on 18.0.0-next.5. Launched there with nothing selected it ran the whole Campaign dialog, then refused in English and sent nothing. It now declares list_item only: every row's menu, plus the selection bar All Leads opens through bulkActions, which fans out one run per ticked row. Both doors measured enrolling in the browser as the marketing user. The contact action is unchanged: its header button never drew, since its visible predicate needs a record. The Opportunity Funnel by Owner -> Stage report's chart was a funnel over amounts that do not shrink stage by stage; it pinched, flared, and hid the Negotiation label. It is now a horizontal-bar chart with the same binding, drawn top to bottom in stage order with every label whole. The report keeps its name, label and description. Docs: the campaign-members and reports pages in three locales. 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 5, 10) · PR #2198 at
Landing: once every check on |
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 5, 10)
Clause-②: no
Items 5 and 10 only. The card stays open for item 6 (its second half) and item 8.
What this does
Item 5, Add to Campaign on a lead.
create_campaignruns on exactly one record, but it declaredlocations: ['list_toolbar', 'list_item']. The console leaves a record-scopedlist_toolbaraction out of the header only on a view whosebulkActionsnames it (objectui#12462), and only All Leads does. On the seven other lead views the header drew Add to Campaign with nothing selected. Clicking it ran the whole Campaign dialog, then refused in English ("This action runs on a single record — select a row first.") and sent nothing.src/sales/actions/lead.actions.ts:create_campaignnow declareslocations: ['list_item']. It keeps two doors: every row's ⋮ menu, and the selection bar that All Leads opens throughbulkActions: ['create_campaign'], which runs once per ticked row. The comment beside the line says whylist_toolbarmust not come back.add_contact_to_campaignis unchanged. Its header button never draws, so the ruling's condition does not hold (see Acceptance notes).Item 10, Opportunity Funnel by Owner → Stage. The report's chart was
type: 'funnel'over amount by stage. The amounts do not shrink stage by stage, so the funnel pinched and flared, and the Negotiation label was hidden.src/sales/reports/opportunity.report.ts: the chart is nowtype: 'horizontal-bar', the same remedy fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) #2182 gave the Pipeline by Stage tile (item 2). Binding, title, filter, rows and values are unchanged. A comment beside the chart line says why.Tests.
test/campaign-enrollment-doors.test.tsgets a new describe with 3 cases.create_campaigndeclares['list_item']and nothing else. All Leads still names it inbulkActions, and it is still the per-record form (type: 'flow',recordIdParam: 'leadId'). One run per ticked row, as the marketing user, enrolls both leads on the one campaign.test/pipeline-by-stage-chart.test.tsgets a new describe with 2 cases. The report's chart ishorizontal-baroverstageandtotal_amountwith the same dataset and filter. It declares noorder, which is the state the stage-order bars were measured in.Docs, three locales each.
marketing/campaign-members: the selection toolbar is named as being on All Leads and All Contacts, and the row menu on any list laid out in rows. The page also says the action is not a header button. zh-Hans uses 全部线索 / 全部联系人 and zh-Hant uses 全部線索 / 全部聯絡人, as the view rosters pin them.analytics/reports: the report's row now says it has a horizontal bar chart of amount by stage across all reps. zh-Hans says 条形图 and zh-Hant says 橫條圖, as fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) #2182 did.Changeset.
.changeset/2073-campaign-header-button-and-funnel-report-chart.md(patch).Item 5: which remedy, by the four axes
The two candidates were these. A: drop
list_toolbarfromlocations. B: wirebulkActions: ['create_campaign']on the lead views that lack it, so that objectui#12462 leaves the header button out there.bulkActionsa grid draws no row checkboxes at all, and kanban, calendar and gallery have no row selection. The admin could succeed only with exactly one row ticked, and that row's ⋮ menu already does the same thing. A loses nothing that works today. B adds multi-row enrollment to four more grids (My Leads, Hot Leads, High Priority, Suspected Duplicates), and nothing measured asks for that. Marketing enrolls from All Leads or with Enroll Members on the campaign.bulkActions, so under B the button comes back on every new view. The three non-grid views have no selection bar to wire.bulkActionson a kanban or gallery view would declare a selection bar that does not render there, only to suppress a header button. That is a declaration the runtime does not honour. A leaves one placement that matches what the action can do.So the choice is A. Every persona that was offered Add to Campaign keeps a working door: the row menu on every lead list laid out in rows, and multi-row through the All Leads selection bar. Both are measured below.
Item 10: the wording call
Readers see "funnel" in four places: the sidebar entry (Opportunity Funnel by Owner, zh-CN 各负责人商机漏斗), the page title, the description ("Per-rep stage-by-stage pipeline funnel") and the chart title (Pipeline Funnel). The report pages are English in every locale, as measured in zh-CN. In each place "funnel" is the sales noun for the stage-by-stage pipeline, and the docs use it that way throughout ("walk through the funnel one stage at a time", "funnel order", "leaky stages"). The defect measured was the chart's shape and its hidden label, not the word.
Renaming would touch the nav label and its three locale packs, twelve doc pages (four pages in three locales) and a test, all outside this claim, with no reader evidence asking for it. So the label, the description and the chart title stay. The comment beside the chart line says so, so the next reader does not "fix" the type to match the word.
Measured
Setup: this worktree,
objectstack devon 18.0.0-next.5 with the shipped seed, and Chromium at/opt/pw-browsers/chromiumat 1440×900. Personas: the dev admin, a marketing user (positionmarketing_user, owning 2 leads), and a sales rep (positionsales_rep). Before ismainb8348e9a, after isbcc0602d. Every number below is a DOM count from a probe script.Item 5, the header button on each view (✓ = Add to Campaign drawn in the list header):
/api/v1/actions/. After: 0 header buttons on that view for all three runs, so there is nothing to open.POST /api/v1/actions/crm_lead/create_campaignwith{"recordId":…,"params":{"campaignId":…}}. It answered 200output.enrolled: true, and the toast read "Leads added to campaign!".enrolled: falsefor the lead the row menu had just enrolled, the otherenrolled: true) and ended "Succeeded 2 / 2". Readingcrm_campaign_memberback gives both leads on the campaign, statussent,created_bythe marketing user.Item 10, opened as the admin from the sidebar's Reports entry:
.recharts-funnel-trapezoid, 139 / 139 / 189 / 1034 / 1034 / 1031 px wide, Prospecting to Closed Won; the Negotiation label not visible.recharts-bar-rectanglebars, 93 / 116 / 116 / 158 / 865 / 862 px long, top to bottom Prospecting, Qualification, Needs Analysis, Proposal, Negotiation, Closed Won; every axis label inside the chartThe chart's query is
dimensions: ['stage'],measures: ['total_amount'], with the report's filter. It answered 200 with 6 rows in alphabetical order of the stored value,closed_wonfirst (139,500 / 174,000 / 173,000 / 237,000 / 1,294,000 / 1,290,000 in stage order), and the console drew them in the stage field's option order. Needs Analysis wraps onto two lines and is drawn whole.Mechanism assumptions
b8348e9a, 1 of the 8 lead list views wiresbulkActions: ['create_campaign'](All Leads,lead.view.ts:223). 7 do not: 4 grids, plus kanban, calendar and gallery. The header comment described this.funnel, on asummaryreport overrows: ['owner', 'stage']withruntimeFilter: { stage: { $ne: 'closed_lost' } }. The spec's report chart isChartConfigSchemaplusxAxisandyAxis, and itstypeenum includesbar,horizontal-bar,columnandfunnel. fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) #2182 expressed the remedy astype: 'horizontal-bar'with nosortBy, so the order comes from the stage options. This report has noordereither.marketing/campaign-membersdescribed the doors and is updated. The report is named inanalytics/reports,analytics/index,sales/pipeline-managementandgetting-started/quick-tour(three locales each). Only theanalytics/reportsrow described its content, and it is updated. The others name the report, which keeps its name. Intest/,docs-setup-navigation-names.test.ts:123names the report in a comment, and no test pinnedlist_toolbaron this action.Tests and gates (head bcc0602)
pnpm verify, run 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 buildcreate_campaignor the report; "0i18n/missing-*issues"; "source hygiene clean"; src/sales business semantics ~57,484 tokens (ceiling ~88,500); build complete with 8 author-time warnings, none naming either item;dist/objectstack.jsoncarriescreate_campaignwithlocations: ['list_item']and the report chart ashorizontal-barvitest run --shard=1/3 --maxWorkers=2vitest run --shard=2/3 --maxWorkers=2it.skipIfintest/script-main-guard.test.ts)vitest run --shard=3/3 --maxWorkers=2The shards hold 202 files, the count
find test -name '*.test.ts'gives. A control-byte scan of the 11 changed files exited 1 (no hit), and a positive control exited 0.Ablation, one per changed behaviour, run once and not kept. Both ran on bcc0602 through
scripts/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:locations: ['list_toolbar', 'list_item']. The anchor went 1 → 0 and the blob 04c90471 → 258c3a05.test/campaign-enrollment-doors.test.tsgave "Tests 1 failed | 31 passed (32)". That is the predicted case,expected [ 'list_toolbar', 'list_item' ] to deeply equal [ 'list_item' ].type: 'funnel'. The anchor went 1 → 0 and the blob 56bbeb81 → 3a006879.test/pipeline-by-stage-chart.test.tsgave "Tests 1 failed | 9 passed (10)". That is the predicted case,expected { type: 'funnel', … } to match object { type: 'horizontal-bar', … }.HEAD:again, andgit diff HEADis 0 bytes.Acceptance notes
ActionRunner, the param dialog comes before the handler, andresolveServerActionRecordIdrefuses there (packages/core/src/actions/serverActionHandler.ts). The two refusal strings are hard-coded English with no i18n key. Measured before this PR: the zh-CN browser showed the English refusal after a localized dialog. objectui#12331 pinned the refusal after the dialog as working. With this PR no HotCRM action reaches that path.add_contact_to_campaignstill declareslist_toolbar, but itsvisiblereadsrecord.email_opt_out. That holds no value in a list header, so the button drew on none of the 3 contact views, for the marketing user or the admin, before or after. The admin's run had one row ticked on Primary Contacts too. The ruling's condition, that it draws, is false. A relaxed predicate would bring item 5 back on contacts, so dropping it too is put to the seat as a question.bar, and its axis shows "Qualificati…" and "Needs Analy…" at 1440 px on a 1044 px chart with 6 categories. The console rotates any category label longer than 5 characters and cuts a rotated one at 12, whatever the width. That is reported to the seat; it is outside this claim.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-013 describes the multi-select bulk action, which stays. OPP-017 counts the funnel report among four, and it keeps its name.Generated by Claude Code