Skip to content

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 into
mainfrom
claude/issue-2073-items-5-10-campaign-toolbar-funnel-report
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-2073-items-5-10-campaign-toolbar-funnel-report

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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_campaign runs on exactly one record, but it declared locations: ['list_toolbar', 'list_item']. The console leaves a record-scoped list_toolbar action out of the header only on a view whose bulkActions names 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_campaign now declares locations: ['list_item']. It keeps two doors: every row's ⋮ menu, and the selection bar that All Leads opens through bulkActions: ['create_campaign'], which runs once per ticked row. The comment beside the line says why list_toolbar must not come back.
  • add_contact_to_campaign is 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.

Tests.

  • test/campaign-enrollment-doors.test.ts gets a new describe with 3 cases. create_campaign declares ['list_item'] and nothing else. All Leads still names it in bulkActions, 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.ts gets a new describe with 2 cases. The report's chart is horizontal-bar over stage and total_amount with the same dataset and filter. It declares no order, 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_toolbar from locations. B: wire bulkActions: ['create_campaign'] on the lead views that lack it, so that objectui#12462 leaves the header button out there.

  • Real business need. Measured as the marketing user, the header button could never succeed on any of the seven views. Marketing has no delete right, so with no bulkActions a 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.
  • Long-term soundness. A fixes the declaration: a single-record action is not drawn where nothing is selected. B leaves the header placement in place and hides it view by view. A user-created view names no bulkActions, so under B the button comes back on every new view. The three non-grid views have no selection bar to wire.
  • Making AI mistakes harder. Under B, bulkActions on 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.
  • No scope growth. A is one value plus its comment. B adds selection-bar wiring to seven views.

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 dev on 18.0.0-next.5 with the shipped seed, and Chromium at /opt/pw-browsers/chromium at 1440×900. Personas: the dev admin, a marketing user (position marketing_user, owning 2 leads), and a sales rep (position sales_rep). Before is main b8348e9a, after is bcc0602d. 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):

view marketing before → after admin before → after sales rep
All Leads – → – (selection bar: Add to Campaign) – → – (selection bar: Add to Campaign) –
My Leads, Hot Leads, High Priority, Suspected Duplicates ✓ → – ✓ → – –
Lead Pipeline, Lead Calendar, Lead Cards ✓ → – ✓ → – –
All Contacts, People Directory, Primary Contacts – → – – → – –
  • Before, on My Leads with nothing selected, for the marketing user in en-US and zh-CN and for the admin: the click opened the dialog ("Campaign*", Select…, Confirm). Picking Cloud Migration Webinar and confirming then toasted "This action runs on a single record — select a row first.", in English under zh-CN too, and 0 requests reached /api/v1/actions/. After: 0 header buttons on that view for all three runs, so there is nothing to open.
  • After, the row menu still lists Add to Campaign on every lead grid that has rows (All Leads, My Leads, Hot Leads, High Priority), for the marketing user and the admin. Suspected Duplicates has no rows on the seed, and the kanban, calendar and gallery views draw no row menu, before or after. As the marketing user on My Leads, the ⋮ menu run sent POST /api/v1/actions/crm_lead/create_campaign with {"recordId":…,"params":{"campaignId":…}}. It answered 200 output.enrolled: true, and the toast read "Leads added to campaign!".
  • After, as the marketing user on All Leads, two rows were ticked. The bar read "2 selected · Add to Campaign · Clear", and its confirm step listed both people. It sent two per-row POSTs (one answered enrolled: false for the lead the row menu had just enrolled, the other enrolled: true) and ended "Succeeded 2 / 2". Reading crm_campaign_member back gives both leads on the campaign, status sent, created_by the marketing user.

Item 10, opened as the admin from the sidebar's Reports entry:

en-US zh-CN
before 6 .recharts-funnel-trapezoid, 139 / 139 / 189 / 1034 / 1034 / 1031 px wide, Prospecting to Closed Won; the Negotiation label not visible the same widths; 谈判 not visible
after 0 trapezoids, 6 .recharts-bar-rectangle bars, 93 / 116 / 116 / 158 / 865 / 862 px long, top to bottom Prospecting, Qualification, Needs Analysis, Proposal, Negotiation, Closed Won; every axis label inside the chart 6 bars, 寻找客户 to 成交, all visible

The 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_won first (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

  1. Holds. On b8348e9a, 1 of the 8 lead list views wires bulkActions: ['create_campaign'] (All Leads, lead.view.ts:223). 7 do not: 4 grids, plus kanban, calendar and gallery. The header comment described this.
  2. Holds on 18.0.0-next.5. The button was drawn for the marketing user and the admin, never for the sales rep, on any of 11 views.
  3. Holds. The chart was funnel, on a summary report over rows: ['owner', 'stage'] with runtimeFilter: { stage: { $ne: 'closed_lost' } }. The spec's report chart is ChartConfigSchema plus xAxis and yAxis, and its type enum includes bar, horizontal-bar, column and funnel. fix(dashboards): Pipeline by Stage is a horizontal bar chart, not a funnel (#2073 item 2) #2182 expressed the remedy as type: 'horizontal-bar' with no sortBy, so the order comes from the stage options. This report has no order either.
  4. Docs and tests. No doc named the header button. marketing/campaign-members described the doors and is updated. The report is named in analytics/reports, analytics/index, sales/pipeline-management and getting-started/quick-tour (three locales each). Only the analytics/reports row described its content, and it is updated. The others name the report, which keeps its name. In test/, docs-setup-navigation-names.test.ts:123 names the report in a comment, and no test pinned list_toolbar on 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's VERDICT command-exit line.

step exit · reading
pnpm validate && pnpm typecheck && pnpm lint && pnpm lint:i18n-gate && pnpm hygiene && pnpm hygiene:tokens && pnpm build 0 · "Validation passed"; 16 lint suggestions, none naming create_campaign or the report; "0 i18n/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.json carries create_campaign with locations: ['list_item'] and the report chart as horizontal-bar
vitest run --shard=1/3 --maxWorkers=2 0 · 68 files, 1115 passed
vitest run --shard=2/3 --maxWorkers=2 0 · 67 files, 1257 passed, 1 skipped (the existing it.skipIf in test/script-main-guard.test.ts)
vitest run --shard=3/3 --maxWorkers=2 0 · 67 files, 1641 passed

The 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.mjs in wrap mode, inside a script trapping git checkout HEAD -- on both absolute paths. No build is needed: the tests read src/ through the composed stack.

  • A1 put list_toolbar back: locations: ['list_toolbar', 'list_item']. The anchor went 1 → 0 and the blob 04c90471 → 258c3a05. test/campaign-enrollment-doors.test.ts gave "Tests 1 failed | 31 passed (32)". That is the predicted case, expected [ 'list_toolbar', 'list_item' ] to deeply equal [ 'list_item' ].
  • A2 put the funnel back: type: 'funnel'. The anchor went 1 → 0 and the blob 56bbeb81 → 3a006879. test/pipeline-by-stage-chart.test.ts gave "Tests 1 failed | 9 passed (10)". That is the predicted case, expected { type: 'funnel', … } to match object { type: 'horizontal-bar', … }.
  • Restore: both blobs are equal to HEAD: again, and git diff HEAD is 0 bytes.

Acceptance notes

  • The console half of item 5 is objectui's, and is reported to the seat, not filed. A record-scoped action launched from the list header collects its params before the selection check runs. In ActionRunner, the param dialog comes before the handler, and resolveServerActionRecordId refuses 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.
  • The contact action is not changed. add_contact_to_campaign still declares list_toolbar, but its visible reads record.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.
  • Seen on the sibling report, not touched: Opportunities by Stage draws a vertical 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.md stays 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

…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>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 20, 2026 11:07am UTC

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Oct 20, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2073 items 5, 10) · PR #2198 at bcc0602d, brought onto main 39dee2be as 45e5e82b · repo:hotcrm seat 2 · os-support-ai · session session_01KqkTQNLcxVc588eZpJmp1r · 2026-10-20T11:10Z · report 6289435817 on hotcrm#2073

  1. Shape:
  2. The diff, read:
    • 5, remedy A: create_campaign declares locations: ['list_item'].
      • The selection-bar door is the view's bulkActions (All Leads), so the action needs no location for it.
      • The measurement decides A over B. On the 7 lead views without bulkActions, the header button could never succeed: the marketing user gets no row checkboxes there, and kanban, calendar and gallery have no selection. B would also come back on every view a user creates.
      • After the change, no lead or contact view draws a header button. The row menu and the selection bar both enroll, as the marketing user.
      • The ⛔ comment records why list_toolbar must not return.
    • 10: the report's chart is horizontal-bar, the remedy item 2 used, with the same binding and title. After the change, 6 bars in stage order, and every label is visible in en-US and zh-CN.
      • Keeping "Funnel" in the name is the dev's wording call. The seat accepts it: the name describes the sales concept, the table and numbers are unchanged, and a rename would reach the nav, 3 packs and 12 docs pages outside the claim.
  3. Changeset and docs, read sentence by sentence:
    • The changeset names the 7 views and the two remaining doors.
    • It says "Contacts were not affected", which the measurement confirms: their header button drew on no contact view.
    • The two docs pages say where Add to Campaign is offered and what the report draws, in all three locales.
  4. Gates:
    • The dev's verify on bcc0602d:
      • the static chain: VERDICT command-exit 0;
      • the three shards: 1115, 1257 + 1 skipped, and 1641 passed;
      • 202 files.
    • One ablation per changed behaviour went red on the predicted case, and each restore is proven blob-equal.
    • CI on 45e5e82b at this review: still running. Landing waits for every check.
  5. The open question, the seat's call: add_contact_to_campaign still declares list_toolbar. Its header button draws nowhere today, only because its visible predicate reads a record. The seat agrees with the dev's A, drop it, but not in this PR: the ruling's condition was false, and the change has no visible effect today. It is folded on Hosted-shape browser pass 2026-10-12 — HotCRM-side P3 defects (8 items: unreachable reports, distorted funnel, escalation task left open, SLA not re-derived, campaign actions, assignment noise, silent account match) — list card, split per item at dispatch #2073 as item 5's follow-up, so the next change to contact.actions.ts carries it.
  6. Out of scope, dispositions:

Landing: once every check on 45e5e82b is green, this seat flips the PR ready and arms auto-merge, bringing main in first if it has moved. After the merge, #2073 goes back to pm:queue for items 6 (second half) and 8, with item 5's contact follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants