Skip to content

Commit

Permalink
feat: [M3-7883] - Add search and alphabetical sorting to Switch Accou…
Browse files Browse the repository at this point in the history
…nt 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
  • Loading branch information
mjac0bs committed May 30, 2024
1 parent 5561201 commit 1154b84
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 12 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10515-added-1716569243109.md
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});

/**
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ describe('SwitchAccountDrawer', () => {
).toBeInTheDocument();
});

it('should have a search bar', () => {
const { getByText } = renderWithTheme(<SwitchAccountDrawer {...props} />);

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', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/manager/src/features/Account/SwitchAccountDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -37,6 +38,7 @@ export const SwitchAccountDrawer = (props: Props) => {
const [isParentTokenError, setIsParentTokenError] = React.useState<
APIError[]
>([]);
const [query, setQuery] = React.useState<string>('');

const isProxyUser = userType === 'proxy';
const currentParentTokenWithBearer =
Expand Down Expand Up @@ -154,13 +156,24 @@ export const SwitchAccountDrawer = (props: Props) => {
)}
.
</Typography>
<DebouncedSearchTextField
clearable
debounceTime={250}
hideLabel
label="Search"
onSearch={setQuery}
placeholder="Search"
sx={{ marginBottom: 3 }}
value={query}
/>
<ChildAccountList
currentTokenWithBearer={
isProxyUser ? currentParentTokenWithBearer : currentTokenWithBearer
}
isLoading={isSubmitting}
onClose={onClose}
onSwitchAccount={handleSwitchToChildAccount}
searchQuery={query}
userType={userType}
/>
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const props = {
currentTokenWithBearer: 'Bearer 123',
onClose: vi.fn(),
onSwitchAccount: vi.fn(),
searchQuery: '',
userType: undefined,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +24,7 @@ interface ChildAccountListProps {
onClose: () => void;
userType: UserType | undefined;
}) => void;
searchQuery: string;
userType: UserType | undefined;
}

Expand All @@ -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,
Expand All @@ -46,8 +56,10 @@ export const ChildAccountList = React.memo(
isError,
isFetchingNextPage,
isInitialLoading,
isRefetching,
refetch: refetchChildAccounts,
} = useChildAccountsInfiniteQuery({
filter,
headers:
userType === 'proxy'
? {
Expand All @@ -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 (
<Box display="flex" justifyContent="center">
<CircleProgress mini size={70} />
Expand All @@ -67,7 +84,13 @@ export const ChildAccountList = React.memo(

if (childAccounts?.length === 0) {
return (
<Notice variant="info">There are no indirect customer accounts.</Notice>
<Notice variant="info">
There are no child accounts
{filter.hasOwnProperty('company')
? ' that match this query'
: undefined}
.
</Notice>
);
}

Expand Down Expand Up @@ -119,11 +142,6 @@ export const ChildAccountList = React.memo(

return (
<Stack alignItems={'flex-start'} data-testid="child-account-list">
{(isSwitchingChildAccounts || isLoading) && (
<Box display="flex" justifyContent="center" width={'100%'}>
<CircleProgress mini size={70} />
</Box>
)}
{!isSwitchingChildAccounts && !isLoading && renderChildAccounts}
{hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
{isFetchingNextPage && <CircleProgress mini />}
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/mocks/serverHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ import {
objectStorageBucketFactory,
objectStorageClusterFactory,
objectStorageKeyFactory,
objectStorageTypeFactory,
objectStorageOverageTypeFactory,
objectStorageTypeFactory,
paymentFactory,
paymentMethodFactory,
placementGroupFactory,
Expand Down

0 comments on commit 1154b84

Please sign in to comment.