Skip to content

Migrate Button to ButtonComposed (batch 5) search+reports - #97336

Open
mikolajpochec wants to merge 8 commits into
Expensify:mainfrom
software-mansion-labs:@mikolajpochec/button-migration-pr5
Open

Migrate Button to ButtonComposed (batch 5) search+reports#97336
mikolajpochec wants to merge 8 commits into
Expensify:mainfrom
software-mansion-labs:@mikolajpochec/button-migration-pr5

Conversation

@mikolajpochec

@mikolajpochec mikolajpochec commented Jul 29, 2026

Copy link
Copy Markdown
Member

Explanation of Change

This is batch 5 (PR5 of 9) of an ongoing effort to migrate every direct <Button> usage (import Button from '@components/Button') over to the new composed ButtonComposed, so the old Button component can eventually be deprecated. This batch covers the "text-only · styled · search+reports" shape: 20 files / 29 button instances.

The migration issue lists 23 files, but three were deleted in ffc0c2baea7 ("Cleanup of screens related to old search filter routes"), taking 4 usages with them:

  • src/components/Search/SearchFiltersAmountBase.tsx
  • src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersReportFieldPage/index.tsx
  • src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWithdrawalTypePage.tsx

Notes for review:

  • extraSmall deleted from the exported ActionCellProps (src/components/Search/SearchList/ListItem/ActionCell/index.tsx, also dropped in .../ActionCell/DeferredActionCell.tsx). Already dead — Migrate ComposedButton to ButtonWithDropdownMenu #93789 removed it from PayActionCell's props and deleted the only caller that passed it in (ReportListItemHeader, which passed !isLargeScreenWidth from inside an isLargeScreenWidth && block, so always false). Both call sites were small={!extraSmall}, so they rendered SMALL before and render SMALL now.
  • Six migrated buttons have no reachable path today, so they were verified by code review rather than on screen:
    • src/components/Search/SearchList/ListItem/ActionCell/index.tsx — the "View" link, now a LinkButton
    • src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx — "Show more", also a LinkButton
    • src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx — "Keep this one"
    • src/components/Search/FilterComponents/DateFilterBase.tsx — "Reset"
    • src/components/Search/SearchFilterPageFooterButtons.tsx — both "Reset" and "Save"

Fixed Issues

$ #95174
PROPOSAL: #83762 (comment)

Tests

For every migrated button, verify that behaviour is unchanged from before the migration:

  1. Label renders correctly and is not clipped or truncated.
  2. Variant/color is correct — success = green, danger = red, default = grey.
  3. Size is correct — height/padding matches the previous look.
  4. Disabled state — the button greys out and is non-interactive when its precondition isn't met.
  5. Loading state — the spinner replaces the label and the button is non-interactive while a request is in flight.
  6. Press action — pressing fires the same action as before.

The migrated buttons fall into a few categories by the styling or behaviour applied to them. Steps for one
representative of each:

Segmented control (Off / Allow / Block)
Preconditions
  1. Be an admin of a Control workspace.
  2. Turn on Rules in Workspaces > Workspace > More features.
Test steps
  1. Open the workspace, copy its ID from the address bar, and go to
    /workspaces/<workspace ID>/rules/spend-rules/new.
  2. Scroll past Permitted currencies and Max amount to the Restrict merchants row.
  3. Click Allow, then Block, then Off.
Expected behavior

The three buttons look like one control — equal heights, edges touching, a single border — matching production.
The selected one is filled (green for Allow, red for Block, plain for Off); the other two are
transparent with grey labels.


Search row action button (color, offline, and inert-but-not-dimmed)
Preconditions
  1. Desktop browser, window wider than 1024 px (the action column doesn't render below that).
  2. At least one report you can submit or approve.
Test steps
  1. Go to the Spend tab and open Reports.
  2. Find a row whose rightmost column shows a green Submit or Approve.
  3. Go offline, then come back online.
  4. Go offline again, reload the page while still offline, then reconnect.
  5. Open the submit to popover from a row and look at the row buttons while it's open.
Expected behavior
  • Green button with the correct label, sized and padded as in production.
  • Offline — whether you went offline on the page or loaded it already offline — greyed out and unclickable. Back
    online it looks normal and works again immediately, with no stuck state.
  • Popover open: the row buttons stop responding but do not grey out.

Button with a loading spinner
Preconditions

At least two expenses not on an approved or paid report.

Test steps
  1. On the Spend tab > Expenses, tick the checkboxes on two rows.
  2. Click the 2 selected header, then Edit multiple.
  3. Click Merchant, type a value, and come back. (Saving with no change just closes the screen — no spinner.)
  4. Press Save and watch the button.
  5. The spinner may appear for a very short time, so to inspect it properly: in
    src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx, change the isSaving line to
    useState(true), reach the screen again, then revert.
Expected behavior

The label is replaced by a centred spinner — hidden, not pushed aside — and the button can't be clicked while it
shows. Then the panel closes.


Button whose label styling comes from the screen using it
Preconditions

None.

Test steps
  1. Open your own chat (marked (you)) from the Inbox.
  2. Click + > Create expense > Manual, enter an amount, and confirm.
  3. Look at the buttons in the message that appears under the new expense.
Expected behavior

Buttons stacked vertically, reading Submit it to someone and Nothing for now — grey, single-line, sized
as in production. (Categorize it / Share it with my accountant may also appear, and there's no green
button here — both are correct.)

For the green variant: in a workspace room, mention a room that doesn't exist (e.g. #does-not-exist-123). The
reply offers a green Yes and a grey No.

Offline tests

QA Steps

Same as tests.

  • 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 actionable_item_buttons_android button_loading_spinner_android
button_variants_small_android.mp4
Android: mWeb Chrome actionable_item_buttons_android_web button_loading_spinner_android_web
button_variants_small_android_web.mp4
iOS: Native actionable_item_buttons_ios button_loading_spinner_ios
button_variants_small_ios.mp4
iOS: mWeb Safari actionable_item_buttons_ios_web button_loading_spinner_ios_web
button_variants_small_ios_web.mp4
MacOS: Chrome / Safari actionable_item_buttons_web button_loading_spinner_web
button_variants_small_web.mp4

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

isNested
medium
size={CONST.BUTTON_SIZE.MEDIUM}
innerStyles={[styles.ph3]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI:

The horizontal padding of this button changed with the migration.

Before: the legacy medium container had paddingHorizontal: 16, and innerStyles={[styles.ph3]} overrode it to 12, with the text flush against that padding - so the label sat 12px from the edge.

After: the new Button already applies ph3 for the medium size, so this innerStyles={[styles.ph3]} is a no-op, and Button.Text always adds its own ph1. The label now sits 16px from the edge and the button is ~8px wider than before.

To keep the old look 1:1:

Suggested change
innerStyles={[styles.ph3]}
innerStyles={[styles.ph2]}

(8px container + 4px text = 12px, same as before.)

Flagging this one specifically because "Show more" is on the "no reachable path" list in the PR description, so the difference was not caught on screen.

Comment on lines 22 to 32
type ActionableItemButtonsProps = {
items: ActionableItem[];
layout?: 'horizontal' | 'vertical';
shouldUseLocalization?: boolean;
primaryTextNumberOfLines?: number;
styles?: {
text?: StyleProp<TextStyle>;
button?: StyleProp<ViewStyle>;
};
wrapperStyle?: StyleProp<ViewStyle>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would change the names of the props to reflect what the new Button uses.

It would be a good idea to use TypeScript's Pick<> for this purpose.

numberOfLines={props.primaryTextNumberOfLines}
style={props.styles?.text}
>
{props.shouldUseLocalization ? translate(item.text as TranslationPaths) : item.text}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NAB, follow-up idea (pre-existing API, not something this PR changed): the shouldUseLocalization flag + text doing double duty (translation key or literal text) could become two mutually exclusive per-item props:

type ActionableItem = {
   isPrimary?: boolean;
     key: string;
     onPress: () => void;
} & ({translationKey: TranslationPaths; text?: never} | {text: string; translationKey?: never});

and the render becomes item.translationKey ? translate(item.translationKey) : item.text with no cast.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great idea, applying this change.

@Guccio163 Guccio163 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Generally good PR, left some minor comments 👀

wrapperStyle?: StyleProp<ViewStyle>;
};

function ActionableItemButtons(props: ActionableItemButtonsProps) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would consider migrating this whole component (ActionableItemButtons) as well - if you look at the usages they are pretty much hardcoded as well, conditionals-dependent. ActionableItemButtons basically passes props down (beside wrapping its children), so look like it could use the composition 🏗️ , WDYT @dariusz-biela ?

large={largeButton}
pressOnEnter
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={largeButton ? CONST.BUTTON_SIZE.LARGE : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd consider changing DateFilterContent's largeButton to optional size - it surely opens doors for passing the 'small' option, but shouldn't be a problem. If concerned, we can always use Exclude/Extract in the prop type definition

text={translate('common.apply')}
pressOnEnter
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={largeButton ? CONST.BUTTON_SIZE.LARGE : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as DateFilterContent 'size' comment

text={translate('common.confirm')}
pressOnEnter
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={largeButton ? CONST.BUTTON_SIZE.LARGE : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as DateFilterContent 'size' comment, if many cases like these occur we can extract new specific type

onPress={() => onSelect(null)}
/>
>
<Button.Text style={[styles.alignSelfCenter, !isOffSelected ? styles.textSupporting : undefined]}>{translate('common.off')}</Button.Text>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

!isOffSelected && styles.textSupporting, accordingly to all similar usages in the file

sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.SPEND_RULE_RESTRICTION_TYPE}
/>
>
<Button.Text style={[styles.alignSelfCenter, !isOffSelected ? styles.textSupporting : undefined]}>{translate('common.off')}</Button.Text>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as in SpendRuleRestrictionTypeToggle.tsx

sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.SPEND_RULE_RESTRICTION_TYPE}
/>
>
<Button.Text style={[styles.alignSelfCenter, !isAllowSelected ? styles.textSupporting : undefined]}>{translate('workspace.rules.spendRules.allow')}</Button.Text>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above

@mikolajpochec
mikolajpochec marked this pull request as ready for review August 3, 2026 15:59
@mikolajpochec
mikolajpochec requested review from a team as code owners August 3, 2026 15:59
@melvin-bot

melvin-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

@ZhenjaHorbach 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 removed request for a team August 3, 2026 15:59

@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: 656888a757

ℹ️ 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".

variant={item.isPrimary ? CONST.BUTTON_VARIANT.SUCCESS : undefined}
{...props.buttonProps}
>
<Button.Text {...props.textProps}>{item.translationKey ? translate(item.translationKey) : item.text}</Button.Text>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor localization for actionable buttons

When callers still pass translation keys in text and set shouldUseLocalization (for example join requests, fraud alerts, mention whispers, and the track-expense buttons in ChatActionableButtons), this branch no longer translates them because it only calls translate() for item.translationKey. Those existing callers were not migrated, so affected buttons will display raw keys such as actionableMentionTrackExpense.submit or common.yes instead of user-facing labels. Either keep honoring shouldUseLocalization here or update all callers to use translationKey.

Useful? React with 👍 / 👎.

selectedDateModifier: SearchDateModifier | null;
hasFeed: boolean;
largeButton?: boolean;
size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'> type hardcodes the string literal 'small'. This value is already defined as the named constant CONST.BUTTON_SIZE.SMALL, so duplicating the raw literal couples this type to the const's value: if CONST.BUTTON_SIZE.SMALL ever changed, the Exclude would silently stop excluding the intended member.

Reference the constant instead:

size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, typeof CONST.BUTTON_SIZE.SMALL>;

Reviewed at: 656888a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

values: Partial<SearchAdvancedFiltersForm> | undefined;
selectedField: PolicyReportField | null;
largeButton?: boolean;
size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'> type hardcodes the string literal 'small' rather than the existing named constant CONST.BUTTON_SIZE.SMALL. Using the raw literal couples the type to the const value and will drift silently if that value changes.

Reference the constant instead:

size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, typeof CONST.BUTTON_SIZE.SMALL>;

Reviewed at: 656888a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

value: string | undefined;
isNegated: boolean;
largeButton?: boolean;
size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CONSISTENCY-2 (docs)

The Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, 'small'> type hardcodes the string literal 'small' instead of the already-imported named constant CONST.BUTTON_SIZE.SMALL. Referencing the literal duplicates the const value and will not stay in sync if the constant changes.

Reference the constant instead:

size?: Exclude<ValueOf<typeof CONST.BUTTON_SIZE>, typeof CONST.BUTTON_SIZE.SMALL>;

Reviewed at: 656888a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No product review required.

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.

4 participants