Skip to content
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: 1 addition & 1 deletion docs/ReferenceArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ See the [`children`](#children) section for more details.

## `children`

By default, `<ReferenceInput>` renders an [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) to let end users select the reference record.
By default, `<ReferenceArrayInput>` renders an [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) to let end users select the reference record.

You can pass a child component to customize the way the reference selector is displayed.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { testDataProvider } from 'ra-core';
import { Basic, WithError } from './ReferenceArrayInputBase.stories';

describe('<ReferenceArrayInputBase>', () => {
afterEach(async () => {
// wait for the getManyAggregate batch to resolve
await waitFor(() => new Promise(resolve => setTimeout(resolve, 0)));
});

it('should pass down the error if any occurred', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render(<WithError />);
await waitFor(() => {
expect(screen.queryByText('Error: fetch error')).not.toBeNull();
});
});
it('should pass the correct resource down to child component', async () => {
render(<Basic />);
// Check that the child component receives the correct resource (tags)
await screen.findByText('Selected tags: 1, 3');
});

it('should provide a ChoicesContext with all available choices', async () => {
render(<Basic />);
await screen.findByText('Total tags: 5');
});

it('should apply default values', async () => {
render(<Basic />);
// Check that the default values are applied (1, 3)
await screen.findByText('Selected tags: 1, 3');
});

it('should accept meta in queryOptions', async () => {
const getList = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: [], total: 25 })
);
const dataProvider = testDataProvider({ getList });
render(<Basic meta dataProvider={dataProvider} />);
await waitFor(() => {
expect(getList).toHaveBeenCalledWith('tags', {
filter: {},
pagination: { page: 1, perPage: 25 },
sort: { field: 'id', order: 'DESC' },
meta: { foo: 'bar' },
signal: undefined,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import * as React from 'react';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';

import { CoreAdmin } from '../../core/CoreAdmin';
import { Resource } from '../../core/Resource';
import { CreateBase } from '../../controller/create/CreateBase';
import { testDataProvider } from '../../dataProvider/testDataProvider';
import { DataProvider } from '../../types';
import { Form } from '../../form/Form';
import { useInput } from '../../form/useInput';
import { InputProps } from '../../form/types';
import { TestMemoryRouter } from '../../routing/TestMemoryRouter';
import {
ReferenceArrayInputBase,
ReferenceArrayInputBaseProps,
} from './ReferenceArrayInputBase';
import {
ChoicesContextValue,
ChoicesProps,
useChoicesContext,
} from '../../form';
import { useGetRecordRepresentation } from '../..';

export default { title: 'ra-core/controller/ReferenceArrayInputBase' };

const tags = [
{ id: 0, name: '3D' },
{ id: 1, name: 'Architecture' },
{ id: 2, name: 'Design' },
{ id: 3, name: 'Painting' },
{ id: 4, name: 'Photography' },
];

const defaultDataProvider = testDataProvider({
getList: () =>
// @ts-ignore
Promise.resolve({
data: tags,
total: tags.length,
}),
// @ts-ignore
getMany: (resource, params) => {
if (process.env.NODE_ENV !== 'test') {
console.log('getMany', resource, params);
}
return Promise.resolve({
data: params.ids.map(id => tags.find(tag => tag.id === id)),
});
},
});

const i18nProvider = polyglotI18nProvider(() => englishMessages);

const CheckboxGroupInput = (
props: Omit<InputProps, 'source'> & ChoicesProps
) => {
const choicesContext = useChoicesContext(props);

return <CheckboxGroupInputBase {...props} {...choicesContext} />;
};

const CheckboxGroupInputBase = (
props: Omit<InputProps, 'source'> & ChoicesProps & ChoicesContextValue
) => {
const { allChoices, isPending, error, resource, source, total } = props;
const input = useInput({ ...props, source });
const getRecordRepresentation = useGetRecordRepresentation(resource);

if (isPending) {
return <span>Loading...</span>;
}

if (error) {
return <span>Error: {error.message}</span>;
}

return (
<div>
{allChoices.map(choice => (
<label key={choice.id}>
<input
type="checkbox"
// eslint-disable-next-line eqeqeq
checked={input.field.value.some(id => id == choice.id)}
onChange={() => {
const newValue = input.field.value.some(
// eslint-disable-next-line eqeqeq
id => id == choice.id
)
? input.field.value.filter(
// eslint-disable-next-line eqeqeq
id => id != choice.id
)
: [...input.field.value, choice.id];
input.field.onChange(newValue);
}}
/>
{getRecordRepresentation(choice)}
</label>
))}
<div>
Selected {resource}: {input.field.value.join(', ')}
</div>
<div>
Total {resource}: {total}
</div>
</div>
);
};

export const Basic = ({
dataProvider = defaultDataProvider,
meta,
...props
}: Partial<ReferenceArrayInputBaseProps> & {
dataProvider?: DataProvider;
meta?: boolean;
}) => (
<TestMemoryRouter initialEntries={['/posts/create']}>
<CoreAdmin dataProvider={dataProvider} i18nProvider={i18nProvider}>
<Resource
name="posts"
create={
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
<h1>Create Post</h1>
<Form>
<ReferenceArrayInputBase
reference="tags"
resource="posts"
source="tags_ids"
queryOptions={
meta ? { meta: { foo: 'bar' } } : {}
}
{...props}
>
<CheckboxGroupInput />
</ReferenceArrayInputBase>
</Form>
</CreateBase>
}
/>
</CoreAdmin>
</TestMemoryRouter>
);

Basic.args = {
meta: false,
};

Basic.argTypes = {
meta: { control: 'boolean' },
};

export const WithRender = ({
dataProvider = defaultDataProvider,
meta,
...props
}: Partial<ReferenceArrayInputBaseProps> & {
dataProvider?: DataProvider;
meta?: boolean;
}) => (
<TestMemoryRouter initialEntries={['/posts/create']}>
<CoreAdmin dataProvider={dataProvider} i18nProvider={i18nProvider}>
<Resource
name="posts"
create={
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
<h1>Create Post</h1>
<Form>
<ReferenceArrayInputBase
reference="tags"
resource="posts"
source="tags_ids"
queryOptions={
meta ? { meta: { foo: 'bar' } } : {}
}
{...props}
render={context => (
<CheckboxGroupInputBase
{...context}
source="tags_ids"
/>
)}
/>
</Form>
</CreateBase>
}
/>
</CoreAdmin>
</TestMemoryRouter>
);

WithRender.args = {
meta: false,
};

WithRender.argTypes = {
meta: { control: 'boolean' },
};

export const WithError = () => (
<TestMemoryRouter initialEntries={['/posts/create']}>
<CoreAdmin
dataProvider={
{
getList: () => Promise.reject(new Error('fetch error')),
getMany: () =>
Promise.resolve({ data: [{ id: 5, name: 'test1' }] }),
} as unknown as DataProvider
}
i18nProvider={i18nProvider}
>
<Resource
name="posts"
create={
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
<h1>Create Post</h1>
<Form
onSubmit={() => {}}
defaultValues={{ tag_ids: [1, 3] }}
>
<ReferenceArrayInputBase
reference="tags"
resource="posts"
source="tag_ids"
>
<CheckboxGroupInput />
</ReferenceArrayInputBase>
</Form>
</CreateBase>
}
/>
</CoreAdmin>
</TestMemoryRouter>
);
Loading
Loading