324 add search records as a fallback action in case of no results (#9976)

Closes https://github.com/twentyhq/core-team-issues/issues/324

Fixed typos: 'No results' is used when multiple results are expected and
'No result' is used when only one result is expected.


https://github.com/user-attachments/assets/e3655ced-465a-44b1-92af-63878b9d8a94
This commit is contained in:
Raphaël Bosi
2025-02-03 15:54:24 +01:00
committed by GitHub
parent eb0762dc58
commit 49e4484937
10 changed files with 81 additions and 12 deletions

View File

@ -32,6 +32,7 @@ export const CommandMenu = () => {
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommands, matchingNavigateCommands,
fallbackCommands,
} = useMatchingCommandMenuCommands({ } = useMatchingCommandMenuCommands({
commandMenuSearch, commandMenuSearch,
}); });
@ -44,6 +45,7 @@ export const CommandMenu = () => {
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommands, matchingNavigateCommands,
fallbackCommands,
) )
.filter(isDefined); .filter(isDefined);
@ -79,6 +81,10 @@ export const CommandMenu = () => {
.concat(matchingNavigateCommands) .concat(matchingNavigateCommands)
.concat(matchingWorkflowRunGlobalCommands), .concat(matchingWorkflowRunGlobalCommands),
}, },
{
heading: t`Search ''${commandMenuSearch}'' with...`,
items: fallbackCommands,
},
]; ];
return ( return (

View File

@ -111,9 +111,6 @@ export const CommandMenuList = ({
}} }}
> >
{children} {children}
{noResults && !loading && (
<StyledEmpty>No result found</StyledEmpty>
)}
{commandGroups.map(({ heading, items }) => {commandGroups.map(({ heading, items }) =>
items?.length ? ( items?.length ? (
<CommandGroup heading={heading} key={heading}> <CommandGroup heading={heading} key={heading}>
@ -139,6 +136,9 @@ export const CommandMenuList = ({
</CommandGroup> </CommandGroup>
) : null, ) : null,
)} )}
{noResults && !loading && (
<StyledEmpty>No results found</StyledEmpty>
)}
</SelectableList> </SelectableList>
</StyledInnerList> </StyledInnerList>
</ScrollWrapper> </ScrollWrapper>

View File

@ -8,7 +8,6 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { import {
mockCurrentWorkspace, mockCurrentWorkspace,
mockedWorkspaceMemberData, mockedWorkspaceMemberData,
@ -20,14 +19,16 @@ import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { HttpResponse, graphql } from 'msw';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { CommandMenu } from '../CommandMenu'; import { CommandMenu } from '../CommandMenu';
const companiesMock = getCompaniesMock();
const openTimeout = 50; const openTimeout = 50;
const companiesMock = getCompaniesMock();
const ContextStoreDecorator: Decorator = (Story) => { const ContextStoreDecorator: Decorator = (Story) => {
return ( return (
<RecordFiltersComponentInstanceContext.Provider <RecordFiltersComponentInstanceContext.Provider
@ -125,6 +126,45 @@ export const SearchRecordsAction: Story = {
expect(await canvas.findByText('Linkedin')).toBeVisible(); expect(await canvas.findByText('Linkedin')).toBeVisible();
const companyTexts = await canvas.findAllByText('Company'); const companyTexts = await canvas.findAllByText('Company');
expect(companyTexts[0]).toBeVisible(); expect(companyTexts[0]).toBeVisible();
expect(await canvas.findByText(companiesMock[0].name)).toBeVisible(); },
};
export const NoResultsSearchFallback: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout);
await userEvent.type(searchInput, 'Linkedin');
expect(await canvas.findByText('No results found')).toBeVisible();
const searchRecordsButton = await canvas.findByText('Search records');
expect(searchRecordsButton).toBeVisible();
await userEvent.click(searchRecordsButton);
expect(await canvas.findByText('Linkedin')).toBeVisible();
},
parameters: {
msw: {
handlers: [
graphql.query('CombinedSearchRecords', () => {
return HttpResponse.json({
data: {
searchCompanies: {
edges: [
{
node: companiesMock[0],
cursor: null,
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
});
}),
],
},
}, },
}; };

View File

@ -1,3 +1,4 @@
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { import {
ActionMenuEntryScope, ActionMenuEntryScope,
@ -128,6 +129,25 @@ export const useCommandMenuCommands = () => {
hotKeys: actionMenuEntry.hotKeys, hotKeys: actionMenuEntry.hotKeys,
})); }));
const searchRecordsAction = actionMenuEntries.find(
(actionMenuEntry) =>
actionMenuEntry.key === RecordAgnosticActionsKey.SEARCH_RECORDS,
);
const fallbackCommands: Command[] = searchRecordsAction
? [
{
id: searchRecordsAction.key,
label: i18n._(searchRecordsAction.label),
Icon: searchRecordsAction.Icon,
onCommandClick: searchRecordsAction.onClick,
type: CommandType.StandardAction,
scope: CommandScope.Global,
hotKeys: searchRecordsAction.hotKeys,
},
]
: [];
return { return {
copilotCommands, copilotCommands,
navigateCommands, navigateCommands,
@ -136,5 +156,6 @@ export const useCommandMenuCommands = () => {
actionObjectCommands, actionObjectCommands,
workflowRunRecordSelectionCommands, workflowRunRecordSelectionCommands,
workflowRunGlobalCommands, workflowRunGlobalCommands,
fallbackCommands,
}; };
}; };

View File

@ -16,6 +16,7 @@ export const useMatchingCommandMenuCommands = ({
actionGlobalCommands, actionGlobalCommands,
workflowRunRecordSelectionCommands, workflowRunRecordSelectionCommands,
workflowRunGlobalCommands, workflowRunGlobalCommands,
fallbackCommands,
} = useCommandMenuCommands(); } = useCommandMenuCommands();
const matchingNavigateCommands = matchCommands(navigateCommands); const matchingNavigateCommands = matchCommands(navigateCommands);
@ -55,5 +56,6 @@ export const useMatchingCommandMenuCommands = ({
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommands, matchingNavigateCommands,
fallbackCommands: noResults ? fallbackCommands : [],
}; };
}; };

View File

@ -182,7 +182,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
/> />
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />} {showNoResult && <MenuItem text="No results" />}
</SelectableList> </SelectableList>
); );
}; };

View File

@ -132,7 +132,7 @@ export const MultipleSelectDropdown = ({
/> />
); );
})} })}
{showNoResult && <MenuItem text="No result" />} {showNoResult && <MenuItem text="No results" />}
{loadingItems && <DropdownMenuSkeletonItem />} {loadingItems && <DropdownMenuSkeletonItem />}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</SelectableList> </SelectableList>

View File

@ -161,7 +161,7 @@ export const MatchColumnSelect = ({
</React.Fragment> </React.Fragment>
))} ))}
{options?.length === 0 && ( {options?.length === 0 && (
<MenuItem key="No result" text="No result" /> <MenuItem key="No results" text="No results" />
)} )}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </DropdownMenu>

View File

@ -42,7 +42,7 @@ export const CurrencyPickerDropdownSelect = ({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{filteredCurrencies.length === 0 ? ( {filteredCurrencies.length === 0 ? (
<MenuItem text="No result" /> <MenuItem text="No results" />
) : ( ) : (
<> <>
{selectedCurrency && ( {selectedCurrency && (

View File

@ -55,7 +55,7 @@ export const PhoneCountryPickerDropdownSelect = ({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{filteredCountries?.length === 0 ? ( {filteredCountries?.length === 0 ? (
<MenuItem text="No result" /> <MenuItem text="No results" />
) : ( ) : (
<> <>
{selectedCountry && ( {selectedCountry && (