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:
@ -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 (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 : [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
{showNoResult && <MenuItem text="No result" />}
|
{showNoResult && <MenuItem text="No results" />}
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user