Repository navigation
[Vendor matching CC] App: enable/disable imported vendors and bulk actions #98355
Description
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:
- Phase 0 contract
- Phase 1A: policy vendor collection
- Phase 1B: preference persistence and command
- Phase 2: integration, matching, and expense behavior
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 mutatepolicy.connections.*.datadirectly. -
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
- Admins can individually enable/disable imported vendors.
- Admins can select multiple rows and apply Enable or Disable in one action.
- Mixed selection offers both actions where applicable.
- Disabled vendors remain visible in the workspace Vendors page.
- New-expense picker search excludes disabled vendors.
- An already-assigned disabled vendor remains visible as an inactive current value.
- The App has no connection-data dual-read or per-integration vendor-shape branches.
- QBO, Intacct, and Xero end-to-end behavior is validated with Phase 2.
🤖 Posted by Codex, an AI coding agent acting for @Beamanator.
Activity
Beamanator commentedon Aug 11, 2026
@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
MelvinBot commentedon Aug 11, 2026
Frontend plan — enable/disable toggle + bulk action for Vendors
Closest pattern to copy: the
Workspaces > Categoriespage. It already uses the exact same@components/Tablestack the Vendors page uses, plus theImportedFromAccountingSoftwarebanner and the sameButtonWithDropdownMenubulk-selection UX we want — so it's a near drop-in template.Backend planning issue created: Expensify/Expensify#670690 (assigned to⚠️ blockers below.
Beamanatorandheyjennahay), with a prompt for me to plan the backend work. The frontend can't fully land until the backend design lands — see theChanges required
1. Data model — add an
enabledfield to vendorsVendortype (src/types/onyx/Policy.ts:410-422) is{id, name, currency, email}today — noenabled. Addenabled: boolean(pluspendingFields/pendingAction/errorsfor optimistic UI, mirroring categories).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)selectedVendorKeysstate +canSelectMultiple, mirroringWorkspaceCategoriesPage.enabledthrough thevendorRowsmap (currently only{keyForList, name}at lines 46-53).getHeaderButtons()that rendersButtonWithDropdownMenu(variantSUCCESS,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)selectionEnabled/selectedKeys/onRowSelectionChangeto the sharedTable(props already exist on it — used by categories).WorkspaceVendorTableRowDatawithenabled+onToggleEnabled, add anenabledcolumn rendering a<Switch>and anactionscaret column, and set rowsinteractive(currentlyinteractive={false}at line 51).4. New action + API command
src/libs/actions/Policy/Vendor.tswithsetWorkspaceVendorEnabledmirroringsetWorkspaceCategoryEnabled(optimisticpendingFields.enabled = UPDATE, success clears, failure restores + setserrors). One handler serves both the single toggle and the bulk action.SetPolicyVendorsEnabled) + params type.5. Filter disabled vendors out of the coding picker (
IOURequestStepVendor.tsx:74)getMatchingVendors(policy). Filter outenabled === falsehere (or via a new enabled-aware helper). Do not filter thegetMatchingVendors/findVendorByIDlookups used byMoneyRequestView,ViolationsUtils, andModifiedExpenseMessage— 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/Tablethat Vendors already renders, the sameImportedFromAccountingSoftwarebanner, 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 inWorkspaceCategoriesPage.getHeaderButtons()and the per-row<Switch>inWorkspaceCategoriesTableRow.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.
Beamanator commentedon Aug 12, 2026
MelvinBot commentedon Aug 12, 2026
Holding off on the draft PR — the data model is genuinely blocked, not just the API command
BeamanatorI 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
enabledthat an optimistic toggle can safely write to. Categories work because they live in a top-levelPOLICY_CATEGORIESOnyx collection keyed by name —setWorkspaceCategoryEnabledjustMERGEs 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 bygetActiveVendorMatchingVendors, and that surface is a positional array with three different per-integration shapes:connections.quickbooksOnline.data.vendors(array of{id, name, currency, email})connections.intacct.data.vendors(array using avaluefield)connections.xero.data.contacts(an object, not an array)To toggle
enabledoptimistically today I'd have toMERGEinto one of those positional arrays insidepolicy.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:
enabled→ undecided (that's the crux of Expensify/Expensify#670690, which is still open with no backend plan yet), depends on #650596 landing.SetPolicyVendorsEnabledcommand (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/writeenabled.How do you want to proceed?
policyVendors_<policyID>collection keyed by vendor ID (per #650596), vs. keeping it insideconnections.*.data.SetPolicyVendorsEnabledwith{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.
Beamanator commentedon Aug 24, 2026
Let's get back to this this week!
[-][CC Vendor matching] Add enable/disable toggle to Vendor list + bulk action[/-][+][Vendor matching CC] App: enable/disable imported vendors and bulk actions[/+]44 remaining items
melvin-bot commentedon Oct 9, 2026
@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:
Beamanator commentedon Oct 9, 2026
nah i reverted b/c there were some slight issues that could only be resolved with some backend PRs
Beamanator commentedon Oct 12, 2026
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
melvin-bot commentedon Oct 15, 2026
@Beamanator, @ShridharGoel Whoops! This issue is 2 days overdue. Let's get this updated quick!
Beamanator commentedon Oct 16, 2026
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 🚀