Skip to content

Commit

Permalink
feat: allow to use tabbed components in guessers (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelSuwinski committed Sep 29, 2022
1 parent 414d297 commit 56aec68
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 35 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 3.4.0

* Handle multiple file upload
* Allow to use tabbed components in guessers

## 3.3.8

* Fix reference input validation
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"fix": "eslint --ignore-pattern 'lib/*' --ext .ts,.tsx,.js,.md --fix .",
"lint": "eslint --ignore-pattern 'lib/*' --ext .ts,.tsx,.js,.md .",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=1 src",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch src",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=1 --watch src",
"watch": "tsc --watch"
}
}
111 changes: 111 additions & 0 deletions src/CreateGuesser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { AdminContext, FormTab, TextInput } from 'react-admin';
import { Resource } from '@api-platform/api-doc-parser';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import CreateGuesser from './CreateGuesser.js';
import SchemaAnalyzerContext from './SchemaAnalyzerContext.js';
import schemaAnalyzer from './hydra/schemaAnalyzer.js';
import type {
ApiPlatformAdminDataProvider,
ApiPlatformAdminRecord,
} from './types.js';

import { API_FIELDS_DATA } from './__fixtures__/parsedData.js';

const hydraSchemaAnalyzer = schemaAnalyzer();
const dataProvider: ApiPlatformAdminDataProvider = {
getList: () => Promise.resolve({ data: [], total: 0 }),
getMany: () => Promise.resolve({ data: [] }),
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
update: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
updateMany: () => Promise.resolve({ data: [] }),
create: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
delete: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
deleteMany: () => Promise.resolve({ data: [] }),
getOne: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
introspect: () =>
Promise.resolve({
data: {
entrypoint: 'entrypoint',
resources: [
new Resource('users', '/users', {
fields: API_FIELDS_DATA,
readableFields: API_FIELDS_DATA,
writableFields: API_FIELDS_DATA,
parameters: [],
}),
],
},
}),
subscribe: () => Promise.resolve({ data: null }),
unsubscribe: () => Promise.resolve({ data: null }),
};

describe('<CreateGuesser />', () => {
test('renders with custom fields', async () => {
render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<CreateGuesser resource="users">
<TextInput source="id" label="label of id" />
<TextInput source="title" label="label of title" />
<TextInput source="body" label="label of body" />
</CreateGuesser>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);

await waitFor(() => {
expect(screen.queryAllByRole('tab')).toHaveLength(0);
expect(screen.queryByText('label of id')).toBeVisible();
expect(screen.queryByText('label of title')).toBeVisible();
expect(screen.queryByText('label of body')).toBeVisible();
});
});

test.each([0, 1])('renders with tabs', async (tabId) => {
const user = userEvent.setup();

render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<CreateGuesser resource="users">
<FormTab label="FormTab 1">
<TextInput source="id" label="label of id" />
<TextInput source="title" label="label of title" />
</FormTab>
<FormTab label="FormTab 2">
<TextInput source="body" label="label of body" />
</FormTab>
</CreateGuesser>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);
await waitFor(async () => {
expect(screen.queryAllByRole('tab')).toHaveLength(2);
const tab = screen.getAllByRole('tab')[tabId];
if (tab) {
await user.click(tab);
}
if (tabId === 0) {
// First tab, available.
expect(screen.queryByText('label of id')).toBeVisible();
expect(screen.queryByText('label of title')).toBeVisible();
// Second tab, unavailable.
expect(screen.queryByText('label of body')).not.toBeVisible();
} else {
// First tab, unavailable.
expect(screen.queryByText('label of id')).not.toBeVisible();
expect(screen.queryByText('label of title')).not.toBeVisible();
// Second tab, available.
expect(screen.queryByText('label of body')).toBeVisible();
}
});
});
});
31 changes: 23 additions & 8 deletions src/CreateGuesser.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { useCallback } from 'react';
import type { PropsWithChildren, ReactNode } from 'react';
import PropTypes from 'prop-types';
import {
Create,
FileInput,
FormTab,
SimpleForm,
TabbedForm,
useCreate,
useNotify,
useRedirect,
Expand Down Expand Up @@ -55,7 +58,7 @@ export const IntrospectedCreateGuesser = ({
toolbar,
warnWhenUnsavedChanges,
sanitizeEmptyValues,
simpleFormComponent,
formComponent,
children,
...props
}: IntrospectedCreateGuesserProps) => {
Expand All @@ -77,10 +80,16 @@ export const IntrospectedCreateGuesser = ({
displayOverrideCode(getOverrideCode(schema, writableFields));
}

const hasFileField = inputChildren.some(
(child) =>
typeof child === 'object' && 'type' in child && child.type === FileInput,
);
const hasFileFieldElement = (elements: Array<ReactNode>): boolean =>
elements.some(
(child) =>
React.isValidElement(child) &&
(child.type === FileInput ||
hasFileFieldElement(
React.Children.toArray((child.props as PropsWithChildren).children),
)),
);
const hasFileField = hasFileFieldElement(inputChildren);

const save = useCallback(
async (values: Partial<RaRecord>) => {
Expand Down Expand Up @@ -150,18 +159,24 @@ export const IntrospectedCreateGuesser = ({
],
);

const hasFormTab = inputChildren.some(
(child) =>
typeof child === 'object' && 'type' in child && child.type === FormTab,
);
const FormType = hasFormTab ? TabbedForm : SimpleForm;

return (
<Create resource={resource} {...props}>
<SimpleForm
<FormType
onSubmit={save}
mode={mode}
defaultValues={defaultValues}
validate={validate}
toolbar={toolbar}
warnWhenUnsavedChanges={warnWhenUnsavedChanges}
component={simpleFormComponent}>
component={formComponent}>
{inputChildren}
</SimpleForm>
</FormType>
</Create>
);
};
Expand Down
131 changes: 131 additions & 0 deletions src/EditGuesser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import { AdminContext, FormTab, TextInput } from 'react-admin';
import { Resource } from '@api-platform/api-doc-parser';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import EditGuesser from './EditGuesser.js';
import SchemaAnalyzerContext from './SchemaAnalyzerContext.js';
import schemaAnalyzer from './hydra/schemaAnalyzer.js';
import type {
ApiPlatformAdminDataProvider,
ApiPlatformAdminRecord,
} from './types.js';

import { API_FIELDS_DATA } from './__fixtures__/parsedData.js';

const hydraSchemaAnalyzer = schemaAnalyzer();
const dataProvider: ApiPlatformAdminDataProvider = {
getList: () => Promise.resolve({ data: [], total: 0 }),
getMany: () => Promise.resolve({ data: [] }),
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
update: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
updateMany: () => Promise.resolve({ data: [] }),
create: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
delete: <RecordType extends ApiPlatformAdminRecord>() =>
Promise.resolve({ data: { id: 'id' } } as { data: RecordType }),
deleteMany: () => Promise.resolve({ data: [] }),
getOne: () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Promise.resolve({
data: {
id: '/users/123',
fieldA: 'fieldA value',
fieldB: 'fieldB value',
deprecatedField: 'deprecatedField value',
title: 'Title',
body: 'Body',
},
}),
introspect: () =>
Promise.resolve({
data: {
entrypoint: 'entrypoint',
resources: [
new Resource('users', '/users', {
fields: API_FIELDS_DATA,
readableFields: API_FIELDS_DATA,
writableFields: API_FIELDS_DATA,
parameters: [],
}),
],
},
}),
subscribe: () => Promise.resolve({ data: null }),
unsubscribe: () => Promise.resolve({ data: null }),
};

describe('<EditGuesser />', () => {
test('renders with custom fields', async () => {
render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<EditGuesser resource="users" id="/users/123">
<TextInput source="id" label="label of id" />
<TextInput source="title" label="label of title" />
<TextInput source="body" label="label of body" />
</EditGuesser>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);

await waitFor(() => {
expect(screen.queryAllByRole('tab')).toHaveLength(0);
expect(screen.queryByText('label of id')).toBeVisible();
expect(screen.queryByLabelText('label of id')).toHaveValue('/users/123');
expect(screen.queryByText('label of title')).toBeVisible();
expect(screen.queryByLabelText('label of title')).toHaveValue('Title');
expect(screen.queryByText('label of body')).toBeVisible();
expect(screen.queryByLabelText('label of body')).toHaveValue('Body');
});
});

test.each([0, 1])('renders with tabs', async (tabId) => {
const user = userEvent.setup();

render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<EditGuesser resource="users" id="/users/123">
<FormTab label="FormTab 1">
<TextInput source="id" label="label of id" />
<TextInput source="title" label="label of title" />
</FormTab>
<FormTab label="FormTab 2">
<TextInput source="body" label="label of body" />
</FormTab>
</EditGuesser>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);

await waitFor(async () => {
expect(screen.queryAllByRole('tab')).toHaveLength(2);
const tab = screen.getAllByRole('tab')[tabId];
if (tab) {
await user.click(tab);
}
if (tabId === 0) {
// First tab, available.
expect(screen.queryByText('label of id')).toBeVisible();
expect(screen.queryByLabelText('label of id')).toHaveValue(
'/users/123',
);
expect(screen.queryByText('label of title')).toBeVisible();
expect(screen.queryByLabelText('label of title')).toHaveValue('Title');
// Second tab, unavailable.
expect(screen.queryByText('label of body')).not.toBeVisible();
} else {
// First tab, unavailable.
expect(screen.queryByText('label of id')).not.toBeVisible();
expect(screen.queryByText('label of title')).not.toBeVisible();
// Second tab, available.
expect(screen.queryByText('label of body')).toBeVisible();
expect(screen.queryByLabelText('label of body')).toHaveValue('Body');
}
});
});
});
Loading

0 comments on commit 56aec68

Please sign in to comment.