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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 3.4.0

* Add enum input guesser
* Handle multiple file upload
* Allow to use tabbed components in guessers
* Use native react-admin `sanitizeEmptyValues`
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const config: Config.InitialOptions = {
setupFilesAfterEnv: ['./jest.setup.ts'],
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['/node_modules/', '/lib/'],
maxWorkers: 1,
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"license": "MIT",
"sideEffects": false,
"dependencies": {
"@api-platform/api-doc-parser": "^0.15.4",
"@api-platform/api-doc-parser": "^0.16.1",
"history": "^5.0.0",
"jsonld": "^8.1.0",
"lodash.isplainobject": "^4.0.6",
Expand Down Expand Up @@ -68,8 +68,8 @@
"eslint-check": "eslint-config-prettier .eslintrc.cjs",
"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 --maxWorkers=1 --watch src",
"test": "NODE_OPTIONS=--experimental-vm-modules jest src",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch src",
"watch": "tsc --watch"
}
}
98 changes: 98 additions & 0 deletions src/InputGuesser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const dataProvider: ApiPlatformAdminDataProvider = {
address: '16 avenue de Rivoli',
},
],
formatType: 'https://schema.org/EBook',
status: 'AVAILABLE',
},
}),
introspect: () =>
Expand Down Expand Up @@ -240,4 +242,100 @@ describe('<InputGuesser />', () => {
});
});
});

test.each([
// Default enum names.
{
transformEnum: undefined,
enums: {
formatType: [
'Https://schema.org/ebook',
'Https://schema.org/audiobookformat',
'Https://schema.org/hardcover',
],
status: ['Available', 'Sold out'],
},
},
// Custom transformation.
{
transformEnum: (value: string | number): string =>
`${value}`
.split('/')
.slice(-1)[0]
?.replace(/([a-z])([A-Z])/, '$1_$2')
.toUpperCase() ?? '',
enums: {
formatType: ['EBOOK', 'AUDIOBOOK_FORMAT', 'HARDCOVER'],
status: ['AVAILABLE', 'SOLD_OUT'],
},
},
])(
'renders enum input with transformation',
async ({ transformEnum, enums }) => {
let updatedData = {};

render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<ResourceContextProvider value="users">
<Edit id="/users/123" mutationMode="pessimistic">
<SimpleForm
onSubmit={(data: {
formatType?: string | null;
status?: string | null;
}) => {
updatedData = data;
}}>
<InputGuesser
transformEnum={transformEnum}
source="formatType"
/>
<InputGuesser transformEnum={transformEnum} source="status" />
</SimpleForm>
</Edit>
</ResourceContextProvider>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);

// eslint-disable-next-line no-restricted-syntax
for (const [fieldId, options] of Object.entries(enums)) {
// eslint-disable-next-line no-await-in-loop
const field = await screen.findByLabelText(
`resources.users.fields.${fieldId}`,
);
expect(field).toBeVisible();
if (field) {
fireEvent.mouseDown(field);
}
// First option is selected.
expect(
screen.queryAllByRole('option', { name: options[0], selected: true })
.length,
).toEqual(1);
expect(
screen.queryAllByRole('option', { selected: false }).length,
).toEqual(options.length);

// eslint-disable-next-line @typescript-eslint/no-loop-func
options.forEach((option) => {
expect(
screen.queryAllByRole('option', { name: option }).length,
).toEqual(1);
});
// Select last option.
const lastOption = screen.getByText(options.slice(-1)[0] ?? '');
fireEvent.click(lastOption);
}

const saveButton = screen.getByRole('button', { name: 'ra.action.save' });
fireEvent.click(saveButton);
await waitFor(() => {
expect(updatedData).toMatchObject({
formatType: 'https://schema.org/Hardcover',
status: 'SOLD_OUT',
});
});
},
);
});
21 changes: 21 additions & 0 deletions src/InputGuesser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const IntrospectedInputGuesser = ({
schema,
schemaAnalyzer,
validate,
transformEnum,
...props
}: IntrospectedInputGuesserProps) => {
const field = fields.find(({ name }) => name === props.source);
Expand Down Expand Up @@ -105,6 +106,26 @@ export const IntrospectedInputGuesser = ({
let parse;
const fieldType = schemaAnalyzer.getFieldType(field);

if (field.enum) {
const choices = Object.entries(field.enum).map(([k, v]) => ({
id: v,
name: transformEnum ? transformEnum(v) : k,
}));
return fieldType === 'array' ? (
<SelectArrayInput
validate={guessedValidate}
choices={choices}
{...(props as SelectArrayInputProps)}
/>
) : (
<SelectInput
validate={guessedValidate}
choices={choices}
{...(props as SelectInputProps)}
/>
);
}

if (['integer_id', 'id'].includes(fieldType) || field.name === 'id') {
const prefix = `/${props.resource}/`;

Expand Down
21 changes: 21 additions & 0 deletions src/__fixtures__/parsedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,25 @@ export const API_FIELDS_DATA = [
embedded: EmbeddedResource,
required: false,
}),
new Field('formatType', {
id: 'https://schema.org/BookFormatType',
range: 'http://www.w3.org/2001/XMLSchema#string',
reference: null,
embedded: null,
enum: {
'Https://schema.org/ebook': 'https://schema.org/EBook',
'Https://schema.org/audiobookformat':
'https://schema.org/AudiobookFormat',
'Https://schema.org/hardcover': 'https://schema.org/Hardcover',
},
required: false,
}),
new Field('status', {
id: 'http://localhost/status',
range: 'http://www.w3.org/2001/XMLSchema#string',
reference: null,
embedded: null,
enum: { Available: 'AVAILABLE', 'Sold out': 'SOLD_OUT' },
required: false,
}),
];
8 changes: 6 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,16 @@ type InputProps =
| ReferenceInputProps;

export type IntrospectedInputGuesserProps = Partial<InputProps> &
IntrospectedGuesserProps;
IntrospectedGuesserProps & {
transformEnum?: (value: string | number) => string | number;
};

export type InputGuesserProps = Omit<
InputProps & Omit<BaseIntrospecterProps, 'resource'>,
'component'
>;
> & {
transformEnum?: (value: string | number) => string | number;
};

export type IntrospecterProps = (
| CreateGuesserProps
Expand Down