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,