Skip to content

Vendor matching CC: Add Vendors tab to Workspace settings#96143

Open
Beamanator wants to merge 13 commits into
mainfrom
beaman-vendors-tab
Open

Vendor matching CC: Add Vendors tab to Workspace settings#96143
Beamanator wants to merge 13 commits into
mainfrom
beaman-vendors-tab

Conversation

@Beamanator

@Beamanator Beamanator commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Adds a read-only Vendors tab under Workspace settings for workspaces on the vendorMatching beta with either QBO (Credit/Debit-card export) or Sage Intacct (Credit Card Charge export) connected. The list is sourced from the same Onyx data already used by the vendor selector RHP shipped in App #91886, so this change is App-only — no backend work is required. The tab shows one column (vendor name); the GL code column for QBO is a follow-up that also needs a Java AcctNum fetch in QuickbooksOnlineImporter.java — tracked in the same Expensify #650595 as Phase 2. Intacct never has a GL code so it is fully complete under Phase 1.

The visibility gate is canReadPolicyFeature(POLICY_FEATURE.VENDORS) && hasVendorFeature(policy, isBetaEnabled(BETAS.VENDOR_MATCHING)) && isMatchingVendorListLoaded(policy). The role check reuses ROLE_PERMISSION_BUNDLES in PolicyUtils.ts, which derives the ADMIN/AUDITOR bundles from Object.values(CONST.POLICY.POLICY_FEATURE) — adding POLICY_FEATURE.VENDORS gives admins WRITE and auditors READ automatically, no server-side change required. The list-loaded clause is stricter than the inline surfaces (Default vendor row, per-expense Vendor field) because a read-only externally-synced destination with no user CTA reads as broken if it opens empty; the page still ships a belt-and-braces empty state for the edge case where the tab is reached before the list populates.

For non-active workspaces the effect that triggers openPolicyAccountingPage runs in WorkspaceInitialPage, gated on the same read-access check, and the page itself is wrapped in withPolicyConnections so direct-URL access hydrates connections before evaluating the wrapper. Offline and Onyx-hydration-race cases are handled by mirroring withPolicyConnections's isOffline and isLoadingOnyxValue guards.

Fixed Issues

$ https://github.com/Expensify/Expensify/issues/650595

PROPOSAL: N/A (internal feature, no external proposal)

Tests

Prerequisites: a workspace with the vendorMatching beta enabled and either (a) QBO connected with non-reimbursable export destination set to Credit Card or Debit Card, or (b) Sage Intacct connected with Credit Card Charge export.

Happy path

  1. Wait for the initial accounting sync to complete on the test workspace.
  2. Open Settings → Workspaces → the test workspace.
  3. Verify a new Vendors row appears in the workspace navigation (below Categories, above Tags when both are enabled).
  4. Tap the row and verify the Vendors page opens.
  5. Verify the vendor list matches the imported vendor list from the connected accounting system (spot-check three names).
  6. Verify each row has no radio/checkbox — the list is read-only.
  7. Type into the search box and verify the list filters case-insensitively as you type.
  8. Clear the search and verify the full list returns.

Visibility branches

  1. Flip the workspace's non-reimbursable export destination to Vendor Bill (QBO) or Vendor Bill (Intacct) via Workspace → Accounting → ExportCompany card expenses. Return to Workspace settings and verify the Vendors row disappears.
  2. Flip the export destination back to Credit Card / Credit Card Charge and verify the row reappears.
  3. Disable the vendorMatching beta on the workspace and verify the Vendors row is hidden regardless of export destination.

Role-based access (Control workspace)

  1. Add an Auditor to the Control workspace. Log in as the Auditor and verify the Vendors row is visible and the list renders. POLICY.POLICY_FEATURE.VENDORS auto-picks up READ for Auditors via ROLE_PERMISSION_BUNDLES in PolicyUtils.ts.
  2. Add a Card Admin to the same workspace. Log in as the Card Admin and verify the Vendors row is hidden (Card Admins can reach workspace settings via Expensify Card / Company Cards but do not have VENDORS in their permission bundle).

Edge cases

  1. On a workspace that just enabled the beta but where the initial accounting sync hasn't yet completed: open Workspace settings and verify the Vendors row is hidden (the isMatchingVendorListLoaded clause of the gate). After the sync completes, the row appears.
  2. On a workspace you have not previously visited (so policy.connections isn't hydrated on app start): paste /workspaces/<policyID>/vendors directly into the URL bar. Verify the page shows a loading indicator until withPolicyConnections hydrates the connections, then renders the list.
  • Verify that no errors appear in the JS console

Offline tests

  1. Complete steps 1–4 of the Tests section online, then disable your network connection.
  2. Verify the Vendors page still opens from cached Onyx state and the vendor list renders as it did online.
  3. Verify the search box continues to filter the cached list.
  4. Reconnect coverage: on a different workspace you have not visited (so policy.connections isn't hydrated), keep the network disabled and open Workspace settings; verify the Vendors row is hidden. Re-enable the network and verify the row appears without requiring a manual navigation to Accounting — the effect refires on the isOffline transition.
  • Verify that no errors appear in the JS console

QA Steps

Same as Tests, but the QA workspace must have the vendorMatching beta enabled by an engineer beforehand. The tab has no end-user-visible effect outside of Workspace settings, so QA scope is limited to verifying visibility branches and that the list contents match the imported accounting-system vendor list.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Adds a read-only Vendors tab to Workspace settings for workspaces with the
`vendorMatching` beta enabled and either QBO (Credit/Debit-card export) or
Sage Intacct (Credit Card Charge export) connected. Sourced from the
existing `connections.<name>.data.vendors` Onyx data used by the vendor
selector RHP shipped in #91886, so no backend changes are required.

Gate: `hasVendorFeature(policy, isBetaEnabled(BETAS.VENDOR_MATCHING)) &&
isMatchingVendorListLoaded(policy)`. The list-loaded clause is stricter
than the inline surfaces (Default vendor row, per-expense Vendor field)
because a read-only externally-synced list with no user CTA is a dead end
if it opens empty.

Phase 2 (GL code column for QBO) requires a Java `AcctNum` fetch in
`QuickbooksOnlineImporter.java` and follows in a separate change. Intacct
is fully complete under Phase 1 (no GL code in Intacct vendor imports).

See Expensify/Expensify#650595 for the full plan.
@Beamanator Beamanator requested review from a team as code owners July 14, 2026 22:54
@melvin-bot melvin-bot Bot requested review from Krishna2323 and removed request for a team July 14, 2026 22:54
@melvin-bot

melvin-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

@Krishna2323 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot requested review from heyjennahay and removed request for a team July 14, 2026 22:54
@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 5aaa7988ece..75cb4720253 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -4443,7 +4443,7 @@ ${amount} für ${merchant} – ${date}`,
             delete: 'Arbeitsbereich löschen',
             settings: 'Einstellungen',
             categories: 'Kategorien',
-            vendors: 'Anbieter',
+            vendors: 'Lieferanten',
             tags: 'Tags',
             customField1: 'Benutzerdefiniertes Feld 1',
             customField2: 'Benutzerdefiniertes Feld 2',
@@ -6245,10 +6245,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
             reportFieldInitialValueRequiredError: 'Bitte wähle einen Anfangswert für ein Berichtsfeld aus',
             genericFailureMessage: 'Beim Aktualisieren des Berichtfelds ist ein Fehler aufgetreten. Bitte versuche es erneut.',
         },
-        vendors: {
-            emptyTitle: 'Noch keine Anbieter',
-            emptySubtitle: 'Anbieter erscheinen hier, sobald die Synchronisierung mit deiner Buchhaltung abgeschlossen ist.',
-        },
+        vendors: {emptyTitle: 'Noch keine Anbieter', emptySubtitle: 'Lieferanten werden hier angezeigt, nachdem Ihre Buchhaltungssynchronisierung abgeschlossen ist.'},
         tags: {
             tagName: 'Tag-Name',
             requiresTag: 'Mitglieder müssen alle Ausgaben taggen',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 43e17e450d9..00f126dc028 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -6081,10 +6081,7 @@ ${amount} para ${merchant} - ${date}`,
             reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe',
             genericFailureMessage: 'Se ha producido un error al actualizar el campo de informe. Por favor, inténtalo de nuevo.',
         },
-        vendors: {
-            emptyTitle: 'Aún no hay proveedores',
-            emptySubtitle: 'Los proveedores aparecerán aquí cuando se complete la sincronización con tu software de contabilidad.',
-        },
+        vendors: {emptyTitle: 'Todavía no hay proveedores', emptySubtitle: 'Los proveedores aparecerán aquí cuando se complete la sincronización de tu contabilidad.'},
         tags: {
             tagName: 'Nombre de etiqueta',
             requiresTag: 'Los miembros deben etiquetar todos los gastos',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index fccbde49e94..84c2fc411db 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -6272,10 +6272,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
             reportFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale pour le champ de note de frais',
             genericFailureMessage: 'Une erreur s’est produite lors de la mise à jour du champ de note de frais. Veuillez réessayer.',
         },
-        vendors: {
-            emptyTitle: 'Aucun fournisseur pour le moment',
-            emptySubtitle: 'Les fournisseurs apparaîtront ici une fois la synchronisation comptable terminée.',
-        },
+        vendors: {emptyTitle: 'Aucun fournisseur pour le moment', emptySubtitle: 'Les fournisseurs apparaîtront ici une fois la synchronisation comptable terminée.'},
         tags: {
             tagName: 'Nom du tag',
             requiresTag: 'Les membres doivent taguer toutes les dépenses',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 6d384ab2e7b..8f6fc4c5dcd 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -6233,10 +6233,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
             reportFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo del resoconto',
             genericFailureMessage: 'Si è verificato un errore durante l’aggiornamento del campo del report. Riprova.',
         },
-        vendors: {
-            emptyTitle: 'Nessun fornitore ancora',
-            emptySubtitle: 'I fornitori appariranno qui al termine della sincronizzazione con la contabilità.',
-        },
+        vendors: {emptyTitle: 'Nessun fornitore per ora', emptySubtitle: 'I fornitori verranno visualizzati qui dopo il completamento della sincronizzazione contabile.'},
         tags: {
             tagName: 'Nome tag',
             requiresTag: 'I membri devono etichettare tutte le spese',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index a83afda6bfb..c186b53d6fe 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -4392,7 +4392,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
             delete: 'ワークスペースを削除',
             settings: '設定',
             categories: 'カテゴリ',
-            vendors: 'ベンダー',
+            vendors: '仕入先',
             tags: 'タグ',
             customField1: 'カスタムフィールド1',
             customField2: 'カスタムフィールド2',
@@ -6152,10 +6152,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
             reportFieldInitialValueRequiredError: 'レポート項目の初期値を選択してください',
             genericFailureMessage: 'レポートフィールドの更新中にエラーが発生しました。もう一度お試しください。',
         },
-        vendors: {
-            emptyTitle: 'ベンダーはまだありません',
-            emptySubtitle: '会計連携の同期が完了すると、ベンダーがここに表示されます。',
-        },
+        vendors: {emptyTitle: 'まだ取引先がありません', emptySubtitle: '会計システムとの同期が完了すると、ここにベンダーが表示されます。'},
         tags: {
             tagName: 'タグ名',
             requiresTag: 'メンバーはすべての経費にタグを付ける必要があります',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index bc1469d24d4..fa73a2513db 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -6215,10 +6215,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
             reportFieldInitialValueRequiredError: 'Kies een beginwaarde voor een rapportveld',
             genericFailureMessage: 'Er is een fout opgetreden bij het bijwerken van het rapportveld. Probeer het opnieuw.',
         },
-        vendors: {
-            emptyTitle: 'Nog geen leveranciers',
-            emptySubtitle: 'Leveranciers verschijnen hier zodra de synchronisatie met je boekhouding is voltooid.',
-        },
+        vendors: {emptyTitle: 'Nog geen leveranciers', emptySubtitle: 'Leveranciers verschijnen hier zodra je boekhoudkundige synchronisatie is voltooid.'},
         tags: {
             tagName: 'Tagnaam',
             requiresTag: 'Leden moeten alle uitgaven taggen',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 247657f86de..ec825eccf4f 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -6196,10 +6196,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
             reportFieldInitialValueRequiredError: 'Wybierz początkową wartość pola raportu',
             genericFailureMessage: 'Wystąpił błąd podczas aktualizowania pola raportu. Spróbuj ponownie.',
         },
-        vendors: {
-            emptyTitle: 'Brak dostawców',
-            emptySubtitle: 'Dostawcy pojawią się tutaj po zakończeniu synchronizacji z systemem księgowym.',
-        },
+        vendors: {emptyTitle: 'Brak dostawców', emptySubtitle: 'Dostawcy pojawią się tutaj po zakończeniu synchronizacji księgowej.'},
         tags: {
             tagName: 'Nazwa tagu',
             requiresTag: 'Członkowie muszą otagować wszystkie wydatki',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 366a3f203a9..3914f4fad04 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -6208,10 +6208,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
             reportFieldInitialValueRequiredError: 'Escolha um valor inicial para o campo de relatório',
             genericFailureMessage: 'Ocorreu um erro ao atualizar o campo do relatório. Tente novamente.',
         },
-        vendors: {
-            emptyTitle: 'Nenhum fornecedor ainda',
-            emptySubtitle: 'Os fornecedores aparecerão aqui após a conclusão da sincronização contábil.',
-        },
+        vendors: {emptyTitle: 'Nenhum fornecedor ainda', emptySubtitle: 'Os fornecedores aparecerão aqui após a conclusão da sua sincronização contábil.'},
         tags: {
             tagName: 'Nome da tag',
             requiresTag: 'Membros devem marcar todas as despesas',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index fbbc1a8d0e1..0b03467778c 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6028,10 +6028,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
             reportFieldInitialValueRequiredError: '请选择报表字段的初始值',
             genericFailureMessage: '更新报表字段时出错。请重试。',
         },
-        vendors: {
-            emptyTitle: '暂无供应商',
-            emptySubtitle: '会计同步完成后,供应商将显示在这里。',
-        },
+        vendors: {emptyTitle: '尚无供应商', emptySubtitle: '会计同步完成后,供应商将显示在此处。'},
         tags: {
             tagName: '标签名称',
             requiresTag: '成员必须为所有报销添加标签',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79c5adcdb4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
Comment thread src/pages/workspace/vendors/WorkspaceVendorsPage.tsx
- Gate the Vendors menu row on isPolicyAdmin(policy) so it mirrors the
  page's [ADMIN, PAID] accessVariants. Without this, non-admin members
  saw the menu row but tap-through rendered NotFound.
- Add SCREENS.WORKSPACE.VENDORS to WIDE_LAYOUT_INACTIVE_SCREENS so the
  keyboard focus trap is disabled on wide side-by-side layouts, matching
  every other workspace central-pane screen.
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/CONST/index.ts 94.81% <ø> (ø)
src/SCREENS.ts 100.00% <ø> (ø)
...mponents/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts 0.00% <ø> (ø)
...gation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts 100.00% <ø> (ø)
src/libs/Navigation/linkingConfig/config.ts 76.92% <ø> (ø)
src/pages/workspace/withPolicy.tsx 94.11% <ø> (ø)
...ppNavigator/Navigators/WorkspaceSplitNavigator.tsx 8.57% <0.00%> (-0.26%) ⬇️
src/libs/PolicyUtils.ts 77.68% <66.66%> (-0.05%) ⬇️
src/ROUTES.ts 21.21% <0.00%> (-0.08%) ⬇️
src/pages/workspace/WorkspaceInitialPage.tsx 0.00% <0.00%> (ø)
... and 1 more
... and 35 files with indirect coverage changes

@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@Krishna2323

Copy link
Copy Markdown
Contributor

@Beamanator does this require C+ review?

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6a5d12c05

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
Comment thread src/pages/workspace/vendors/WorkspaceVendorsPage.tsx
- Fix isMatchingVendorListLoaded so it returns false for Intacct workspaces
  that haven't synced yet. Previously the Intacct branch of
  getActiveVendorMatchingVendors normalized a missing data.vendors to [],
  which meant the helper returned an array (not undefined) and the tab
  gate opened onto an empty page — the exact pre-sync state we're trying
  to avoid. QBO was already correct because it returned data?.vendors
  directly.
- Set shouldHideSelectionButton on the Vendors page rows so
  SelectableListItem skips the radio button. The list is read-only, so
  showing radio buttons with a no-op onSelectRow made rows look like
  editable pickers with no feedback on tap.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d184b98dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
@Beamanator

Copy link
Copy Markdown
Contributor Author

@Krishna2323 hey hey! I think yes, do you have experience with setting up QBO / other accounting integrations? If so, can you let me know an email address you have for testing this so i can add it to the beta for you to test? 🙏

If not, no worries i can find someone else to help test this!

Only the active policy has connections data at app start (per
withPolicyConnections's own docstring); other workspaces have to fetch
it lazily. hasVendorFeature() and isMatchingVendorListLoaded() both
read `policy.connections`, so on a non-active vendor-matching workspace
the Vendors menu row was staying hidden even after the beta was flipped
on, until the user visited the Accounting tab.

Add an effect that mirrors withPolicyConnections' fetch trigger inline
in WorkspaceInitialPage, gated on the VENDOR_MATCHING beta so we don't
add an accounting-page fetch to every workspace visit for non-beta
users. Same self-gates as withPolicyConnections: bails if connections
are already fetched, or if the workspace has no connections enabled at
all.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de45732ec3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/vendors/WorkspaceVendorsPage.tsx
The previous commit added a connections-prefetch effect to
WorkspaceInitialPage so the sidebar tab renders on non-active workspaces,
but that only covers the sidebar entry point. A user opening or
refreshing /workspaces/:policyID/vendors directly on a non-active
workspace still hit the page before connections were fetched, so
hasVendorFeature() returned false and AccessOrNotFoundWrapper rendered
NotFound.

Wrap WorkspaceVendorsPage in withPolicyConnections so the page itself
triggers the fetch and shows a loading indicator until connections are
available. Also register VENDORS in the PolicyRouteName union used by
withPolicy's route type, matching how CATEGORIES/TAGS/etc. are typed.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9f0095593

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
useOnyx returns undefined for the value during initial hydration, so my
guard on !hasConnectionsDataBeenFetched passed even when the persisted
flag was actually true, causing a redundant openPolicyAccountingPage
call on every workspace-page rehydrate. Mirror withPolicyConnections
by reading the second-tuple result metadata and gating on
isLoadingOnyxValue.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ab6b5b010

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx
…reconnect

Firing openPolicyAccountingPage while offline fails silently and never
sets POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED. When the app reconnected,
none of the effect's deps changed, so the fetch was never retried and
the Vendors row stayed hidden until the user visited Accounting.

Read isOffline from the existing useNetwork call, guard the effect on
it, and add it to the dependency array so the effect re-fires when
connectivity comes back — matching withPolicyConnections' pattern.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90fa1fd349

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
The Vendors menu row is gated on isPolicyAdmin(policy) below, so
firing openPolicyAccountingPage for non-admin viewers of a vendor-
matching-beta workspace wastes a request (or triggers a denied read
that flips POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED to false, inviting
retries). Match the menu row's admin gate before triggering the fetch.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33c64552c2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
isPolicyAdmin(policy) with no login only reads policy.role, which is
often undefined on non-active policies. For an admin whose role only
exists in employeeList[currentUserLogin], the earlier check treated
them as non-admin — skipping the connection-data prefetch and hiding
the Vendors row until they visited Accounting. Pass currentUserLogin
to both the prefetch gate and the menu row gate so getPolicyRole can
fall back to the employeeList entry.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5072eecdb2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/vendors/WorkspaceVendorsPage.tsx Outdated
Codex round 9 flagged that Auditor and other read-only workspace roles
couldn't view the tab even though it's read-only. Follow-up on my
initial pushback: verified that ROLE_PERMISSION_BUNDLES in PolicyUtils
is a purely client-side map derived from CONST.POLICY.POLICY_FEATURE,
so adding VENDORS as a new feature auto-flows through the ADMIN and
AUDITOR bundles (write and read respectively). No backend coordination
required.

- Add POLICY_FEATURE.VENDORS constant
- Swap the page wrapper from accessVariants={[ADMIN, PAID]} to
  accessVariants={[PAID]} + policyFeature={VENDORS}. The wrapper
  auto-drops the ADMIN check when policyFeature is present and gates on
  canMemberRead(policy, login, VENDORS) instead.
- Swap the menu-row gate from isCurrentUserPolicyAdmin to
  canReadPolicyFeature(VENDORS).

Prefetch effect on WorkspaceInitialPage stays admin-only for now — it
triggers openPolicyAccountingPage which we haven't verified is
read-only-safe for Auditors. On Control workspaces the admin will
almost always visit first, populating the connections data before an
Auditor arrives; if QA reports the vendor list not populating for
Auditors, loosen that gate too.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: acb2c17370

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/workspace/WorkspaceInitialPage.tsx Outdated
The previous commit loosened the menu-row and page gates to
canReadPolicyFeature(VENDORS) but kept the connections-prefetch effect
admin-only, self-flagging it as "loosen next if QA reports auditors not
seeing the list". Codex flagged the same before QA got there: an
Auditor viewing a non-active workspace never triggers the prefetch,
hasVendorFeature/isMatchingVendorListLoaded stay false, and the tab
never renders.

Loosen the prefetch to `canReadPolicyFeature(VENDORS)` — same helper
already backs the menu row. `openPolicyAccountingPage` is safe to fire
for non-admins: it's the same fetch used by `withPolicyConnections`
that peer read-only-safe pages (Categories, etc.) hit via their
`policyFeature`-gated wrappers.
@Beamanator

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 92181ca829

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Beamanator Beamanator requested review from thelullabyy and removed request for Krishna2323 July 15, 2026 19:09
@Beamanator

Copy link
Copy Markdown
Contributor Author

Thanks for volunteering to help review @thelullabyy !

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants