diff --git a/packages/manager/.changeset/pr-10681-tech-stories-1721227051502.md b/packages/manager/.changeset/pr-10681-tech-stories-1721227051502.md new file mode 100644 index 00000000000..07bd820d083 --- /dev/null +++ b/packages/manager/.changeset/pr-10681-tech-stories-1721227051502.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Replace 'react-select' with Autocomplete in Billing ([#10681](https://github.com/linode/manager/pull/10681)) diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index 1cdcbcb1b6c..70079a2d4cb 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -170,11 +170,6 @@ describe('Billing Activity Feed', () => { .scrollIntoView() .should('be.visible'); - cy.contains('[data-qa-enhanced-select]', 'All Transaction Types').should( - 'be.visible' - ); - cy.contains('[data-qa-enhanced-select]', '6 Months').should('be.visible'); - // Confirm that payments and invoices from the past 6 months are displayed, // and that payments and invoices beyond 6 months are not displayed. invoiceMocks6Months.forEach((invoice) => @@ -201,11 +196,12 @@ describe('Billing Activity Feed', () => { mockGetInvoices(invoiceMocks).as('getInvoices'); mockGetPayments(paymentMocks).as('getPayments'); - cy.contains('[data-qa-enhanced-select]', '6 Months') + cy.findByText('Transaction Dates').click().type(`All Time`); + ui.autocompletePopper + .findByTitle(`All Time`) .should('be.visible') .click(); - ui.select.findItemByText('All Time').should('be.visible').click(); cy.wait(['@getInvoices', '@getPayments']); // Confirm that all invoices and payments are displayed. @@ -218,12 +214,12 @@ describe('Billing Activity Feed', () => { }); // Change transaction type drop-down to "Payments" only. - cy.contains('[data-qa-enhanced-select]', 'All Transaction Types') + cy.findByText('Transaction Types').click().type(`Payments`); + ui.autocompletePopper + .findByTitle(`Payments`) .should('be.visible') .click(); - ui.select.findItemByText('Payments').should('be.visible').click(); - // Confirm that all payments are shown and that all invoices are hidden. paymentMocks.forEach((payment) => cy.findByText(`Payment #${payment.id}`).should('be.visible') @@ -233,12 +229,12 @@ describe('Billing Activity Feed', () => { ); // Change transaction type drop-down to "Invoices" only. - cy.contains('[data-qa-enhanced-select]', 'Payments') + cy.findByText('Transaction Types').should('be.visible').focused().click(); + ui.autocompletePopper + .findByTitle('Invoices') .should('be.visible') .click(); - ui.select.findItemByText('Invoices').should('be.visible').click(); - // Confirm that all invoices are shown and that all payments are hidden. invoiceMocks6Months.forEach((invoice) => { cy.findByText(invoice.label).should('be.visible'); @@ -272,11 +268,8 @@ describe('Billing Activity Feed', () => { cy.wait(['@getInvoices', '@getPayments', '@getPaymentMethods']); // Change invoice date selection from "6 Months" to "All Time". - cy.contains('[data-qa-enhanced-select]', '6 Months') - .should('be.visible') - .click(); - - ui.select.findItemByText('All Time').should('be.visible').click(); + cy.findByText('Transaction Dates').click().type('All Time'); + ui.autocompletePopper.findByTitle('All Time').should('be.visible').click(); cy.get('[data-qa-billing-activity-panel]') .should('be.visible') diff --git a/packages/manager/src/features/Billing/BillingDetail.tsx b/packages/manager/src/features/Billing/BillingDetail.tsx index 6102f09b7c8..18dcac982f6 100644 --- a/packages/manager/src/features/Billing/BillingDetail.tsx +++ b/packages/manager/src/features/Billing/BillingDetail.tsx @@ -1,6 +1,6 @@ import Paper from '@mui/material/Paper'; -import Grid from '@mui/material/Unstable_Grid2'; import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import { PayPalScriptProvider } from '@paypal/react-paypal-js'; import * as React from 'react'; @@ -14,7 +14,7 @@ import { useAllPaymentMethodsQuery } from 'src/queries/account/payment'; import { useProfile } from 'src/queries/profile/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import BillingActivityPanel from './BillingPanels/BillingActivityPanel/BillingActivityPanel'; +import { BillingActivityPanel } from './BillingPanels/BillingActivityPanel/BillingActivityPanel'; import BillingSummary from './BillingPanels/BillingSummary'; import ContactInfo from './BillingPanels/ContactInfoPanel'; import PaymentInformation from './BillingPanels/PaymentInfoPanel'; @@ -120,5 +120,3 @@ export const BillingActionButton = styled(Button)(({ theme, ...props }) => ({ minWidth: 'auto', padding: 0, })); - -export default BillingDetail; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.test.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.test.tsx index ba8d988fd30..dad0f849f76 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.test.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.test.tsx @@ -5,11 +5,13 @@ import * as React from 'react'; import { invoiceFactory, paymentFactory } from 'src/factories/billing'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import BillingActivityPanel, { +import { + BillingActivityPanel, getCutoffFromDateRange, invoiceToActivityFeedItem, makeFilter, paymentToActivityFeedItem, + transactionDateOptions, } from './BillingActivityPanel'; vi.mock('../../../../utilities/getUserTimezone'); @@ -71,13 +73,13 @@ describe('BillingActivityPanel', () => { }); it('should filter by item type', async () => { - const { queryAllByTestId, queryByTestId, queryByText } = renderWithTheme( + const { getByLabelText, queryByTestId, queryByText } = renderWithTheme( ); // Test selecting "Invoices" await waitFor(() => { - const transactionTypeSelect = queryAllByTestId('select')?.[0]; + const transactionTypeSelect = getByLabelText('Transaction Types'); fireEvent.change(transactionTypeSelect, { target: { value: 'invoice' }, }); @@ -86,7 +88,7 @@ describe('BillingActivityPanel', () => { // Test selecting "Payments" await waitFor(() => { - const transactionTypeSelect = queryAllByTestId('select')?.[0]; + const transactionTypeSelect = getByLabelText('Transaction Types'); fireEvent.change(transactionTypeSelect, { target: { value: 'payment' }, }); @@ -95,12 +97,12 @@ describe('BillingActivityPanel', () => { }); it('should filter by transaction date', async () => { - const { queryAllByTestId, queryByTestId, queryByText } = renderWithTheme( + const { getByLabelText, queryByTestId, queryByText } = renderWithTheme( ); await waitFor(() => { - const transactionDateSelect = queryAllByTestId('select')?.[1]; + const transactionDateSelect = getByLabelText('Transaction Dates'); fireEvent.change(transactionDateSelect, { target: { value: '30 Days' }, }); @@ -110,11 +112,11 @@ describe('BillingActivityPanel', () => { }); it('should display transaction selection components with defaults', async () => { - const { getByText } = renderWithTheme(); - await waitFor(() => { - getByText('All Transaction Types'); - getByText('90 Days'); - }); + const { getByLabelText } = renderWithTheme(); + const transactionTypeSelect = getByLabelText('Transaction Types'); + expect(transactionTypeSelect).toHaveValue('All Transaction Types'); + const transactionDateSelect = getByLabelText('Transaction Dates'); + expect(transactionDateSelect).toHaveValue('6 Months'); }); it('should display "Account active since"', async () => { @@ -173,22 +175,29 @@ describe('paymentToActivityFeedItem', () => { throw new Error('Invalid test date'); } - expect(getCutoffFromDateRange('30 Days', testDateISO)).toBe( - testDate.minus({ days: 30 }).toISO() - ); - expect(getCutoffFromDateRange('60 Days', testDateISO)).toBe( - testDate.minus({ days: 60 }).toISO() - ); - expect(getCutoffFromDateRange('90 Days', testDateISO)).toBe( - testDate.minus({ days: 90 }).toISO() - ); - expect(getCutoffFromDateRange('6 Months', testDateISO)).toBe( - testDate.minus({ months: 6 }).toISO() - ); - expect(getCutoffFromDateRange('12 Months', testDateISO)).toBe( - testDate.minus({ months: 12 }).toISO() - ); - expect(getCutoffFromDateRange('All Time', testDateISO)).toBeNull(); + expect( + getCutoffFromDateRange(transactionDateOptions[0], testDateISO) + ).toBe(testDate.minus({ days: 30 }).toISO()); + + expect( + getCutoffFromDateRange(transactionDateOptions[1], testDateISO) + ).toBe(testDate.minus({ days: 60 }).toISO()); + + expect( + getCutoffFromDateRange(transactionDateOptions[2], testDateISO) + ).toBe(testDate.minus({ days: 90 }).toISO()); + + expect( + getCutoffFromDateRange(transactionDateOptions[3], testDateISO) + ).toBe(testDate.minus({ months: 6 }).toISO()); + + expect( + getCutoffFromDateRange(transactionDateOptions[4], testDateISO) + ).toBe(testDate.minus({ months: 12 }).toISO()); + + expect( + getCutoffFromDateRange(transactionDateOptions[5], testDateISO) + ).toBeNull(); }); }); diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx index 9a7235d8729..de4a54993dc 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx @@ -1,18 +1,14 @@ -import { - Invoice, - InvoiceItem, - Payment, - getInvoiceItems, -} from '@linode/api-v4/lib/account'; -import { Theme, styled } from '@mui/material/styles'; +import { getInvoiceItems } from '@linode/api-v4/lib/account'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import { DateTime } from 'luxon'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Currency } from 'src/components/Currency'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; import { Link } from 'src/components/Link'; import OrderBy from 'src/components/OrderBy'; @@ -47,9 +43,16 @@ import { getAll } from 'src/utilities/getAll'; import { getTaxID } from '../../billingUtils'; +import type { Invoice, InvoiceItem, Payment } from '@linode/api-v4/lib/account'; +import type { Theme } from '@mui/material/styles'; + const useStyles = makeStyles()((theme: Theme) => ({ activeSince: { - marginRight: theme.spacing(1.25), + marginBottom: theme.spacing(1), + marginTop: theme.spacing(1), + [theme.breakpoints.down('sm')]: { + marginBottom: theme.spacing(2), + }, }, dateColumn: { width: '25%', @@ -57,16 +60,8 @@ const useStyles = makeStyles()((theme: Theme) => ({ descriptionColumn: { width: '25%', }, - flexContainer: { - alignItems: 'center', - display: 'flex', - flexDirection: 'row', - }, headerContainer: { - alignItems: 'center', - backgroundColor: theme.color.white, display: 'flex', - flexDirection: 'row', justifyContent: 'space-between', [theme.breakpoints.down('sm')]: { alignItems: 'flex-start', @@ -75,9 +70,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, headerLeft: { display: 'flex', - flexGrow: 2, - marginLeft: 10, - paddingLeft: 20, + flexDirection: 'column', [theme.breakpoints.down('sm')]: { paddingLeft: 0, }, @@ -87,21 +80,12 @@ const useStyles = makeStyles()((theme: Theme) => ({ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', - padding: 5, + paddingRight: 20, [theme.breakpoints.down('sm')]: { alignItems: 'flex-start', flexDirection: 'column', - marginLeft: 15, - paddingLeft: 0, }, }, - headline: { - fontSize: '1rem', - lineHeight: '1.5rem', - marginBottom: 8, - marginLeft: 15, - marginTop: 8, - }, pdfDownloadColumn: { '& > .loading': { width: 115, @@ -111,9 +95,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ pdfError: { color: theme.color.red, }, - root: { - padding: '8px 0', - }, totalColumn: { [theme.breakpoints.up('md')]: { textAlign: 'right', @@ -121,6 +102,9 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, }, transactionDate: { + [theme.breakpoints.down('sm')]: { + marginTop: theme.spacing(1), + }, width: 130, }, transactionType: { @@ -137,21 +121,29 @@ interface ActivityFeedItem { type: 'invoice' | 'payment'; } -type TransactionTypes = 'all' | ActivityFeedItem['type']; -const transactionTypeOptions: Item[] = [ +interface TransactionTypeOptions { + label: string; + value: 'all' | 'invoice' | 'payment'; +} + +const transactionTypeOptions: TransactionTypeOptions[] = [ { label: 'Invoices', value: 'invoice' }, { label: 'Payments', value: 'payment' }, { label: 'All Transaction Types', value: 'all' }, ]; -type DateRange = - | '6 Months' - | '12 Months' - | '30 Days' - | '60 Days' - | '90 Days' - | 'All Time'; -const transactionDateOptions: Item[] = [ +interface TransactionDateOptions { + label: string; + value: + | '6 Months' + | '12 Months' + | '30 Days' + | '60 Days' + | '90 Days' + | 'All Time'; +} + +export const transactionDateOptions: TransactionDateOptions[] = [ { label: '30 Days', value: '30 Days' }, { label: '60 Days', value: '60 Days' }, { label: '90 Days', value: '90 Days' }, @@ -160,8 +152,6 @@ const transactionDateOptions: Item[] = [ { label: 'All Time', value: 'All Time' }, ]; -const defaultDateRange: DateRange = '6 Months'; - const AkamaiBillingInvoiceText = ( Charges in the final Akamai invoice should be considered the final source @@ -182,30 +172,26 @@ export interface Props { accountActiveSince?: string; } -export const BillingActivityPanel = (props: Props) => { +export const BillingActivityPanel = React.memo((props: Props) => { const { accountActiveSince } = props; - const { data: profile } = useProfile(); const { data: account } = useAccount(); const { data: regions } = useRegionsQuery(); - const isAkamaiCustomer = account?.billing_source === 'akamai'; - const { classes } = useStyles(); const flags = useFlags(); - const pdfErrors = useSet(); const pdfLoading = useSet(); const [ selectedTransactionType, setSelectedTransactionType, - ] = React.useState('all'); + ] = React.useState(transactionTypeOptions[2]); const [ selectedTransactionDate, setSelectedTransactionDate, - ] = React.useState(defaultDateRange); + ] = React.useState(transactionDateOptions[3]); const endDate = getCutoffFromDateRange(selectedTransactionDate); const filter = makeFilter(endDate); @@ -299,25 +285,6 @@ export const BillingActivityPanel = (props: Props) => { [payments, flags, account, pdfErrors] ); - // Handlers for thisOption.value === selectedTransactionType - ) || null - } - className={classes.transactionType} - hideLabel - inline - isClearable={false} - isSearchable={false} - label="Transaction Types" - onChange={handleTransactionTypeChange} - options={transactionTypeOptions} - small - /> -