Skip to content

[Vendor matching CC] App: enable/disable imported vendors and bulk actions #98355

Open

Description

@heyjennahay

Parent tracker: Expensify/Expensify#678247

Role in the phased plan

This is the App workstream for imported-vendor enable/disable controls.

It consumes the agreed policyVendors_{policyID} collection and the SetPolicyVendorsEnabled command. It may begin after Phase 0 establishes that contract, but functional completion/release depends on the Phase 1 backend work and Phase 2 end-to-end integration.

Related work:

Product behavior

The workspace Vendors page must show all currently imported vendors, including disabled vendors, so workspace admins can re-enable them.

For new expenses, disabled vendors must not be available in vendor search or selection. If an expense already has a vendor that later becomes disabled, retain and visibly mark that current historical value, but do not expose it as a general picker result. The user may select an enabled replacement.

Scope

  • Read policyVendors_{policyID}; do not read or mutate policy.connections.*.data directly.

  • Add an enabled/disabled toggle to each imported-vendor row.

  • Add row selection and bulk Enable/Disable actions, following the Workspace Categories table and bulk-action pattern.

  • Use one App action for both individual and bulk changes, calling:

    SetPolicyVendorsEnabled({
        policyID,
        vendorIDs: string[],
        enabled: boolean,
    })
  • Apply optimistic updates and error handling against policyVendors_{policyID}.

  • Do not offer Disable for the current default vendor. The backend remains authoritative and rejects a request containing it.

  • Update the expense vendor picker to consume enabled entries only, with the explicit current-disabled-vendor display exception above.

  • Add the required translated copy and accessibility labels.

Out of scope

  • Persisting disabledVendors.
  • Publishing the policy vendor collection.
  • Sync lifecycle cleanup.
  • Server-side automatic matching and inactive-vendor violations.

Those belong to the linked backend/Phase 2 issues.

Acceptance criteria

  1. Admins can individually enable/disable imported vendors.
  2. Admins can select multiple rows and apply Enable or Disable in one action.
  3. Mixed selection offers both actions where applicable.
  4. Disabled vendors remain visible in the workspace Vendors page.
  5. New-expense picker search excludes disabled vendors.
  6. An already-assigned disabled vendor remains visible as an inactive current value.
  7. The App has no connection-data dual-read or per-integration vendor-shape branches.
  8. QBO, Intacct, and Xero end-to-end behavior is validated with Phase 2.

🤖 Posted by Codex, an AI coding agent acting for @Beamanator.

Activity

  1. self-assigned this
    on Aug 11, 2026
  2. self-assigned this
    on Aug 11, 2026
  3. Beamanator commented on Aug 11, 2026

    @Beamanator
    Contributor

    @MelvinBot plan out the required changes for this issue. Also make an issue in Expensify/Expensify repo in order to plan out and implement the backend changes for this project, and assign me and Jenna there too.

    Sees that issue with a prompt for MelvinBot to plan out the backend changes for all the features planned in this issue

  4. MelvinBot commented on Aug 11, 2026

    @MelvinBot
    Contributor

    Frontend plan — enable/disable toggle + bulk action for Vendors

    Closest pattern to copy: the Workspaces > Categories page. It already uses the exact same @components/Table stack the Vendors page uses, plus the ImportedFromAccountingSoftware banner and the same ButtonWithDropdownMenu bulk-selection UX we want — so it's a near drop-in template.

    Backend planning issue created: Expensify/Expensify#670690 (assigned to Beamanator and heyjennahay), with a prompt for me to plan the backend work. The frontend can't fully land until the backend design lands — see the ⚠️ blockers below.

    Changes required

    1. Data model — add an enabled field to vendors

    • The Vendor type (src/types/onyx/Policy.ts:410-422) is {id, name, currency, email} today — no enabled. Add enabled: boolean (plus pendingFields/pendingAction/errors for optimistic UI, mirroring categories).
    • ⚠️ Blocked on backend: vendors currently live inside policy.connections.<integration>.data, which accounting sync overwrites — so the enabled-state persistence location is a backend design decision (tracked in #670690, coordinated with the top-level-Onyx-key refactor Expensify/Expensify#650596). The frontend Onyx read shape depends on the outcome.

    2. Page — add selection mode + bulk dropdown (WorkspaceVendorsPage.tsx)

    • Add selectedVendorKeys state + canSelectMultiple, mirroring WorkspaceCategoriesPage.
    • Carry enabled through the vendorRows map (currently only {keyForList, name} at lines 46-53).
    • Add a getHeaderButtons() that renders ButtonWithDropdownMenu (variant SUCCESS, customText={translate('workspace.common.selected', {count})}) when 2+ rows are selected. Push a Disable option when any selected vendor is enabled and an Enable option when any selected is disabled — so a mixed selection shows both, exactly as the issue describes.

    3. Table — enable per-row selection + toggle (WorkspaceVendorsTable/index.tsx)

    • Pass selectionEnabled / selectedKeys / onRowSelectionChange to the shared Table (props already exist on it — used by categories).
    • Extend WorkspaceVendorTableRowData with enabled + onToggleEnabled, add an enabled column rendering a <Switch> and an actions caret column, and set rows interactive (currently interactive={false} at line 51).

    4. New action + API command

    • Add src/libs/actions/Policy/Vendor.ts with setWorkspaceVendorEnabled mirroring setWorkspaceCategoryEnabled (optimistic pendingFields.enabled = UPDATE, success clears, failure restores + sets errors). One handler serves both the single toggle and the bulk action.
    • Register a new write command (working name SetPolicyVendorsEnabled) + params type. ⚠️ Blocked on backend — this command doesn't exist yet; name/params/response come from #670690.

    5. Filter disabled vendors out of the coding picker (IOURequestStepVendor.tsx:74)

    • The picker builds its list from getMatchingVendors(policy). Filter out enabled === false here (or via a new enabled-aware helper). Do not filter the getMatchingVendors/findVendorByID lookups used by MoneyRequestView, ViolationsUtils, and ModifiedExpenseMessage — historical transactions must still show their (now-disabled) vendor name.

    6. Copy — add translation strings for the enable/disable menu items, toggle labels, and the "X selected" button in src/languages/*.ts.

    Why Categories is the right template (vs. Tags/Taxes/ReportFields)

    Tags/Taxes/ReportFields use the older SelectionListWithModal; copying them would also mean swapping out the Vendors table component. Categories uses the same modern @components/Table that Vendors already renders, the same ImportedFromAccountingSoftware banner, and the same accounting-connection sourcing — so the diff is additive rather than a rewrite. The mixed-selection "show both Enable and Disable" logic already exists in WorkspaceCategoriesPage.getHeaderButtons() and the per-row <Switch> in WorkspaceCategoriesTableRow.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/98355#issuecomment-5258533964) to create a draft PR (best done once the backend command/persistence shape from Expensify/Expensify#670690 is settled).
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's plan.

  5. Beamanator commented on Aug 12, 2026

    @Beamanator
    Contributor
  6. MelvinBot commented on Aug 12, 2026

    @MelvinBot
    Contributor

    Holding off on the draft PR — the data model is genuinely blocked, not just the API command

    Beamanator I dug into the code to start implementing, and the blocker is deeper than "the write command doesn't exist yet." The persistence shape the whole feature hangs on is undecided, so anything I build now would be a throwaway that fails QA. Here's the concrete problem:

    There is nowhere to store enabled that an optimistic toggle can safely write to. Categories work because they live in a top-level POLICY_CATEGORIES Onyx collection keyed by name — setWorkspaceCategoryEnabled just MERGEs into ${POLICY_CATEGORIES}${policyID}. Vendors have no such key (grep POLICY_VENDORS src/ONYXKEYS.ts → nothing). They're read straight out of the connection blob by getActiveVendorMatchingVendors, and that surface is a positional array with three different per-integration shapes:

    • QBO → connections.quickbooksOnline.data.vendors (array of {id, name, currency, email})
    • Sage Intacct → connections.intacct.data.vendors (array using a value field)
    • Xero → connections.xero.data.contacts (an object, not an array)

    To toggle enabled optimistically today I'd have to MERGE into one of those positional arrays inside policy.connections.<integration>.data — which (a) is overwritten by the next accounting sync, i.e. the exact "toggle must survive re-sync" bug this issue exists to prevent, and (b) needs three different code paths. That's what Expensify/Expensify#650596 ("pull vendor lists into top-level Onyx keys") is meant to fix first — and it's still open.

    Status of the two things I'd need:

    • Persistence location for enabled → undecided (that's the crux of Expensify/Expensify#670690, which is still open with no backend plan yet), depends on #650596 landing.
    • SetPolicyVendorsEnabled command (name/params/response) → doesn't exist; only per-integration default-vendor commands exist today.

    Everything else in the plan (selection mode, the header dropdown, the per-row <Switch>, the picker filter, copy) is cheap and additive once those two are nailed down — but they're all cosmetic without a real place to read/write enabled.

    How do you want to proceed?

    1. Wait for the backend shape (recommended in the original plan) — once #670690 settles the Onyx key + command, I can land the whole frontend in one clean PR mirroring Categories.
    2. Unblock me with a working assumption — if you're happy to commit to a target shape now, reply with it and I'll build the optimistic UI against it as a draft (flagged for reconciliation). The two decisions I need are:
      • Onyx key: e.g. a new top-level policyVendors_<policyID> collection keyed by vendor ID (per #650596), vs. keeping it inside connections.*.data.
      • Command contract: e.g. SetPolicyVendorsEnabled with {policyID, vendorIDs: string[], enabled: boolean}.

    I'd rather not open a PR that toggles a field the backend never persists and calls a command that 500s — say the word on option 2 with the shape you want, or point me at the settled #670690 and I'll implement immediately.

  7. Beamanator commented on Aug 24, 2026

    @Beamanator
    Contributor

    Let's get back to this this week!

  8. changed the title [-][CC Vendor matching] Add enable/disable toggle to Vendor list + bulk action[/-] [+][Vendor matching CC] App: enable/disable imported vendors and bulk actions[/+] on Sep 2, 2026
  9. 44 remaining items

  10. melvin-bot commented on Oct 9, 2026

    @melvin-bot

    @Beamanator @ShridharGoel

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.6-6 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

  11. Beamanator commented on Oct 9, 2026

    @Beamanator
    Contributor

    nah i reverted b/c there were some slight issues that could only be resolved with some backend PRs

  12. Beamanator commented on Oct 12, 2026

    @Beamanator
    Contributor

    still waiting on https://github.com/Expensify/Auth/pull/25323 to get deployed to unblock the Web-E PR, THEN we can get the App PR rewritten

  13. pinned and unpinned a comment on Oct 12, 2026
  14. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    @Beamanator, @ShridharGoel Whoops! This issue is 2 days overdue. Let's get this updated quick!

  15. Beamanator commented on Oct 16, 2026

    @Beamanator
    Contributor

    https://github.com/Expensify/Auth/pull/25323 deployed, now the Web-E PR is merged and on staging, THEN we can get the App PR back 🚀

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

Metadata

Metadata

Labels

ImprovementItem broken or needs improvement.ReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions