From 1154b849627a986ff494f35c1d597866b63265ef Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 30 May 2024 07:07:37 -0700 Subject: [PATCH] feat: [M3-7883] - Add search and alphabetical sorting to Switch Account drawer (#10515) * Add search bar to filter on child accounts * Sort child accounts alphabetically * Update ChildAccountList unit test * Update SwitchAccountDrawer unit test * Update cypress test coverage * Clean up and add a todo to address flakiness * Fix order of mock requests in test, thanks @jdamore-linode * Use the correct event handler and clean up filtering * Handle loading state with search * Update terminology * Added changeset: Alphabetical account sorting and search capabilities to Switch Account drawer * Address feedback: use API filtering and revert client-side sort * Clean up; fix notice copy * Address feedback: @hkhalil-akamai, @bnussman-akamai --- .../pr-10515-added-1716569243109.md | 5 ++ .../parentChild/account-switching.spec.ts | 70 ++++++++++++++++++- .../Account/SwitchAccountDrawer.test.tsx | 6 ++ .../features/Account/SwitchAccountDrawer.tsx | 13 ++++ .../SwitchAccounts/ChildAccountList.test.tsx | 1 + .../SwitchAccounts/ChildAccountList.tsx | 34 ++++++--- packages/manager/src/mocks/serverHandlers.ts | 2 +- 7 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 packages/manager/.changeset/pr-10515-added-1716569243109.md diff --git a/packages/manager/.changeset/pr-10515-added-1716569243109.md b/packages/manager/.changeset/pr-10515-added-1716569243109.md new file mode 100644 index 00000000000..dff7b36cd31 --- /dev/null +++ b/packages/manager/.changeset/pr-10515-added-1716569243109.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Alphabetical account sorting and search capabilities to Switch Account drawer ([#10515](https://github.com/linode/manager/pull/10515)) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index c20ba294325..cfb59e42e07 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -307,6 +307,72 @@ describe('Parent/Child account switching', () => { mockChildAccount.company ); }); + + /* + * - Confirms search functionality in the account switching drawer. + */ + it('can search child accounts', () => { + mockGetProfile(mockParentProfile); + mockGetAccount(mockParentAccount); + mockGetChildAccounts([mockChildAccount, mockAlternateChildAccount]); + mockGetUser(mockParentUser); + + cy.visitWithLogin('/'); + cy.trackPageVisit().as('pageVisit'); + + // Confirm that Parent account username and company name are shown in user + // menu button, then click the button. + assertUserMenuButton( + mockParentProfile.username, + mockParentAccount.company + ).click(); + + // Click "Switch Account" button in user menu. + ui.userMenu + .find() + .should('be.visible') + .within(() => { + ui.button + .findByTitle('Switch Account') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm search functionality. + ui.drawer + .findByTitle('Switch Account') + .should('be.visible') + .within(() => { + // Confirm all child accounts are displayed when drawer loads. + cy.findByText(mockChildAccount.company).should('be.visible'); + cy.findByText(mockAlternateChildAccount.company).should('be.visible'); + + // Confirm no results message. + mockGetChildAccounts([]).as('getEmptySearchResults'); + cy.findByPlaceholderText('Search').click().type('Fake Name'); + cy.wait('@getEmptySearchResults'); + + cy.contains(mockChildAccount.company).should('not.exist'); + cy.findByText( + 'There are no child accounts that match this query.' + ).should('be.visible'); + + // Confirm filtering by company name displays only one search result. + mockGetChildAccounts([mockChildAccount]).as('getSearchResults'); + cy.findByPlaceholderText('Search') + .click() + .clear() + .type(mockChildAccount.company); + cy.wait('@getSearchResults'); + + cy.findByText(mockChildAccount.company).should('be.visible'); + cy.contains(mockAlternateChildAccount.company).should('not.exist'); + cy.contains( + 'There are no child accounts that match this query.' + ).should('not.exist'); + }); + }); }); /** @@ -378,9 +444,7 @@ describe('Parent/Child account switching', () => { .findByTitle('Switch Account') .should('be.visible') .within(() => { - cy.findByText('There are no indirect customer accounts.').should( - 'be.visible' - ); + cy.findByText('There are no child accounts.').should('be.visible'); cy.findByText('switch back to your account') .should('be.visible') .click(); diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx index aa9e88ebe1b..aca09b3f886 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx @@ -29,6 +29,12 @@ describe('SwitchAccountDrawer', () => { ).toBeInTheDocument(); }); + it('should have a search bar', () => { + const { getByText } = renderWithTheme(); + + expect(getByText('Search')).toBeVisible(); + }); + it('should include a link to switch back to the parent account if the active user is a proxy user', async () => { server.use( http.get('*/profile', () => { diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index dbcdc3ad084..50f546ae1de 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; +import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; @@ -37,6 +38,7 @@ export const SwitchAccountDrawer = (props: Props) => { const [isParentTokenError, setIsParentTokenError] = React.useState< APIError[] >([]); + const [query, setQuery] = React.useState(''); const isProxyUser = userType === 'proxy'; const currentParentTokenWithBearer = @@ -154,6 +156,16 @@ export const SwitchAccountDrawer = (props: Props) => { )} . + { isLoading={isSubmitting} onClose={onClose} onSwitchAccount={handleSwitchToChildAccount} + searchQuery={query} userType={userType} /> diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx index 8aea8a621d4..3e3416e2426 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx @@ -11,6 +11,7 @@ const props = { currentTokenWithBearer: 'Bearer 123', onClose: vi.fn(), onSwitchAccount: vi.fn(), + searchQuery: '', userType: undefined, }; diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx index c6dfa3b3cea..7803c0c7573 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx @@ -11,7 +11,7 @@ import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; import { useChildAccountsInfiniteQuery } from 'src/queries/account/account'; -import type { UserType } from '@linode/api-v4'; +import type { Filter, UserType } from '@linode/api-v4'; interface ChildAccountListProps { currentTokenWithBearer: string; @@ -24,6 +24,7 @@ interface ChildAccountListProps { onClose: () => void; userType: UserType | undefined; }) => void; + searchQuery: string; userType: UserType | undefined; } @@ -33,8 +34,17 @@ export const ChildAccountList = React.memo( isLoading, onClose, onSwitchAccount, + searchQuery, userType, }: ChildAccountListProps) => { + const filter: Filter = { + ['+order']: 'asc', + ['+order_by']: 'company', + }; + if (searchQuery) { + filter['company'] = { '+contains': searchQuery }; + } + const [ isSwitchingChildAccounts, setIsSwitchingChildAccounts, @@ -46,8 +56,10 @@ export const ChildAccountList = React.memo( isError, isFetchingNextPage, isInitialLoading, + isRefetching, refetch: refetchChildAccounts, } = useChildAccountsInfiniteQuery({ + filter, headers: userType === 'proxy' ? { @@ -57,7 +69,12 @@ export const ChildAccountList = React.memo( }); const childAccounts = data?.pages.flatMap((page) => page.data); - if (isInitialLoading) { + if ( + isInitialLoading || + isLoading || + isSwitchingChildAccounts || + isRefetching + ) { return ( @@ -67,7 +84,13 @@ export const ChildAccountList = React.memo( if (childAccounts?.length === 0) { return ( - There are no indirect customer accounts. + + There are no child accounts + {filter.hasOwnProperty('company') + ? ' that match this query' + : undefined} + . + ); } @@ -119,11 +142,6 @@ export const ChildAccountList = React.memo( return ( - {(isSwitchingChildAccounts || isLoading) && ( - - - - )} {!isSwitchingChildAccounts && !isLoading && renderChildAccounts} {hasNextPage && fetchNextPage()} />} {isFetchingNextPage && } diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 8cc5bb94aa7..a125c54c7f9 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -76,8 +76,8 @@ import { objectStorageBucketFactory, objectStorageClusterFactory, objectStorageKeyFactory, - objectStorageTypeFactory, objectStorageOverageTypeFactory, + objectStorageTypeFactory, paymentFactory, paymentMethodFactory, placementGroupFactory,