Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update <Datagrid> and <SimpleList> empty message when a filter is active #10184

Merged
merged 18 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ra-core/src/i18n/TranslationMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface TranslationMessages extends StringMap {
navigation: {
[key: string]: StringMap | string;
no_results: string;
no_filtered_results: string;
clear_filters: string;
no_more_results: string;
page_out_of_boundaries: string;
page_out_from_end: string;
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-language-english/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ const englishMessages: TranslationMessages = {
"Some of your changes weren't saved. Are you sure you want to ignore them?",
},
navigation: {
no_results: 'No results found',
clear_filters: 'Clear filters',
no_filtered_results:
'No %{resource} found using the current filters.',
no_results: 'No %{resource} found',
no_more_results:
'The page number %{page} is out of boundaries. Try the previous page.',
page_out_of_boundaries: 'Page number %{page} out of boundaries',
Expand Down
3 changes: 3 additions & 0 deletions packages/ra-language-french/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ const frenchMessages: TranslationMessages = {
"Certains changements n'ont pas été enregistrés. Êtes-vous sûr(e) de vouloir quitter cette page ?",
},
navigation: {
clear_filters: 'Effacer les filtres',
no_filtered_results:
'Aucun résultat trouvé avec les filtres actuels.',
no_results: 'Aucun résultat',
no_more_results:
'La page numéro %{page} est en dehors des limites. Essayez la page précédente.',
Expand Down
17 changes: 17 additions & 0 deletions packages/ra-ui-materialui/src/list/ListNoResults.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { NoFilter, WithFilter } from './ListNoResults.stories';

describe('ListNoResults', () => {
it('should display no results found message when no filter', async () => {
render(<NoFilter />);
await screen.findByText('No results found.');
});

it('should display no results found message and a clear filter link when there is a filter', async () => {
render(<WithFilter />);
await screen.findByText('No results found with the current filters.');
screen.getByText('Clear filters').click();
await screen.findByText('{"id":1}');
});
});
36 changes: 36 additions & 0 deletions packages/ra-ui-materialui/src/list/ListNoResults.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { useList, ListContextProvider } from 'ra-core';
import { ThemeProvider, createTheme } from '@mui/material';
import { ListNoResults } from './ListNoResults';

export default {
title: 'ra-ui-materialui/list/ListNoResults',
};

export const NoFilter = () => {
const context = useList<any>({ data: [] });
return (
<ListContextProvider value={context}>
{context.data?.length === 0 && <ListNoResults />}
</ListContextProvider>
);
};

export const WithFilter = () => {
const context = useList<any>({ data: [{ id: 1 }], filter: { id: 2 } });
return (
<ThemeProvider theme={createTheme()}>
<ListContextProvider value={context}>
{context.data?.length === 0 ? (
<ListNoResults />
) : (
<ul>
{context.data?.map(record => (
<li key={record.id}>{JSON.stringify(record)}</li>
))}
</ul>
)}
</ListContextProvider>
</ThemeProvider>
);
};
33 changes: 26 additions & 7 deletions packages/ra-ui-materialui/src/list/ListNoResults.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import * as React from 'react';
import { memo } from 'react';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import { useResourceContext, useTranslate } from 'ra-core';
import { CardContent, Typography } from '@mui/material';
import { useListContext, useResourceContext, useTranslate } from 'ra-core';

export const ListNoResults = memo(() => {
import { Button } from '../button';

export const ListNoResults = () => {
const translate = useTranslate();
const resource = useResourceContext();
const { filterValues, setFilters } = useListContext();
return (
<CardContent>
<Typography variant="body2">
{translate('ra.navigation.no_results', { resource })}
{filterValues && Object.keys(filterValues).length > 0 ? (
<>
{translate('ra.navigation.no_filtered_results', {
resource,
_: 'No results found with the current filters.',
})}{' '}
<Button
onClick={() => setFilters({}, [])}
label={translate('ra.navigation.clear_filters', {
_: 'Clear filters',
})}
/>
</>
) : (
translate('ra.navigation.no_results', {
resource,
_: 'No results found.',
})
)}
</Typography>
</CardContent>
);
});
};
45 changes: 43 additions & 2 deletions packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as React from 'react';
import { render, screen, waitFor, within } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import { ListContext, ResourceContextProvider } from 'ra-core';

import { AdminContext } from '../../AdminContext';
import { SimpleList } from './SimpleList';
import { TextField } from '../../field/TextField';
import { NoPrimaryText } from './SimpleList.stories';
import { Basic } from '../filter/FilterButton.stories';

const Wrapper = ({ children }: any) => (
<AdminContext>
Expand Down Expand Up @@ -143,11 +150,45 @@ describe('<SimpleList />', () => {
}}
>
<SimpleList />
</ListContext.Provider>
</ListContext.Provider>,
{ wrapper: Wrapper }
);
expect(screen.queryByText('ra.navigation.no_results')).not.toBeNull();
});

it('should display a message when there is no result but filters applied', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this test in the FilterButton tests file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we test the <SimpleList> logic here. I use the FilterButton's story to be able to test it with writing a new component. Do you prefer I write a new component to render that test ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then, why the SimpleList and not the List? Should we do both? @fzaninotto

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, the List?oResults is used by SimpleList and Datagrid, so in theory we should test both. Instead, I added a test for ListNoResults itself.

render(<Basic />);

await screen.findByText(
'Accusantium qui nihil voluptatum quia voluptas maxime ab similique'
);

fireEvent.change(screen.getByLabelText('Search'), {
target: { value: 'w' },
});

expect(
await screen.findByText('No posts found using the current filters.')
).not.toBeNull();
expect(screen.getByText('Clear filters')).not.toBeNull();

fireEvent.click(screen.getByText('Clear filters'));

await screen.findByText(
'Accusantium qui nihil voluptatum quia voluptas maxime ab similique'
);

expect(
screen.queryByText('No posts found using the current filters.')
).toBeNull();
expect(screen.queryByText('Clear filters')).toBeNull();
expect(
screen.queryByText(
'In facilis aut aut odit hic doloribus. Fugit possimus perspiciatis sit molestias in. Sunt dignissimos sed quis at vitae veniam amet. Sint sunt perspiciatis quis doloribus aperiam numquam consequatur et. Blanditiis aut earum incidunt eos magnam et voluptatem. Minima iure voluptatum autem. At eaque sit aperiam minima aut in illum.'
)
).not.toBeNull();
});

it('should fall back to record representation when no primaryText is provided', async () => {
render(<NoPrimaryText />);
await screen.findByText('War and Peace');
Expand Down
Loading