Skip to content

Commit

Permalink
feat: enum field guesser (#489)
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelSuwinski authored Dec 9, 2022
1 parent c46f207 commit bf00773
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/EnumField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ArrayField,
SingleFieldList,
TextField,
useRecordContext,
} from 'react-admin';
import type { TextFieldProps } from 'react-admin';
import type { EnumFieldProps } from './types.js';

const EnumField = ({ transformEnum, source, ...props }: EnumFieldProps) => {
const record = useRecordContext();

if (!record || typeof source === 'undefined') {
return null;
}

const value = record[source];
const enumRecord = {
[source]: (Array.isArray(value) ? value : [value]).map((v) => ({
value: transformEnum ? transformEnum(v) : v,
})),
};

return (
<ArrayField source={source} record={enumRecord}>
<SingleFieldList linkType={false}>
<TextField {...(props as TextFieldProps)} source="value" />
</SingleFieldList>
</ArrayField>
);
};

EnumField.displayName = 'EnumField';

EnumField.propTypes = {
...TextField.propTypes,
transformEnum: PropTypes.func,
};

export default EnumField;
118 changes: 118 additions & 0 deletions src/FieldGuesser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import { AdminContext, Show } from 'react-admin';
import { Resource } from '@api-platform/api-doc-parser';
import { render, screen, waitFor } from '@testing-library/react';

import FieldGuesser from './FieldGuesser.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: '/users/123' } } 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',
description: 'Lorem ipsum dolor sit amet',
nullText: null,
embedded: {
address: '91 rue du Temple',
},
embeddeds: [
{
address: '16 avenue de Rivoli',
},
],
formatType: 'https://schema.org/EBook',
status: 'AVAILABLE',
genre: ['MYTH', 'FAIRY_TALE'],
},
}),
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('<FieldGuesser />', () => {
test.each([
// Default enum names.
{
transformEnum: undefined,
expectedValues: [
'Https://schema.org/ebook',
'Available',
'Myth',
'Fairy tale',
],
},
// Custom transformation.
{
transformEnum: (value: string | number): string =>
`${value}`
.split('/')
.slice(-1)[0]
?.replace(/([a-z])([A-Z])/, '$1_$2')
.toUpperCase() ?? '',
expectedValues: ['EBOOK', 'AVAILABLE', 'MYTH', 'FAIRY_TALE'],
},
])(
'renders enum fields with transformation',
async ({ transformEnum, expectedValues }) => {
const props = transformEnum ? { transformEnum } : {};
render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<Show resource="users" id="/users/123">
<FieldGuesser source="title" />
<FieldGuesser source="formatType" {...props} />
<FieldGuesser source="status" {...props} />
<FieldGuesser source="genre" {...props} />
</Show>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);
await waitFor(() => {
expect(screen.queryAllByText('Title')).toHaveLength(1);
expectedValues.forEach((value) => {
expect(screen.queryAllByText(value)).toHaveLength(1);
});
});
},
);
});
14 changes: 14 additions & 0 deletions src/FieldGuesser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import type {
import type { Field, Resource } from '@api-platform/api-doc-parser';

import Introspecter from './Introspecter.js';
import EnumField from './EnumField.js';
import type {
EnumFieldProps,
FieldGuesserProps,
FieldProps,
IntrospectedFieldGuesserProps,
Expand Down Expand Up @@ -87,6 +89,18 @@ const renderField = (
);
}

if (field.enum) {
return (
<EnumField
transformEnum={(value) =>
Object.entries(field.enum ?? {}).find(([, v]) => v === value)?.[0] ??
value
}
{...(props as EnumFieldProps)}
/>
);
}

const fieldType = schemaAnalyzer.getFieldType(field);

switch (fieldType) {
Expand Down
9 changes: 9 additions & 0 deletions src/__fixtures__/parsedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,13 @@ export const API_FIELDS_DATA = [
enum: { Available: 'AVAILABLE', 'Sold out': 'SOLD_OUT' },
required: false,
}),
new Field('genre', {
id: 'http://localhost/tags',
range: 'http://www.w3.org/2001/XMLSchema#array',
reference: null,
embedded: null,
maxCardinality: null,
enum: { Epic: 'EPIC', 'Fairy tale': 'FAIRY_TALE', Myth: 'MYTH' },
required: false,
}),
];
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,13 @@ export type FieldProps =
| EmailFieldProps
| ArrayFieldProps
| ReferenceArrayFieldProps
| EnumFieldProps
| ReferenceFieldProps;

export type EnumFieldProps = TextFieldProps & {
transformEnum?: (value: string | number) => string | number;
};

export type IntrospectedFieldGuesserProps = FieldProps &
IntrospectedGuesserProps;

Expand Down

0 comments on commit bf00773

Please sign in to comment.