diff --git a/CHANGELOG.md b/CHANGELOG.md index 8475734f..9ccb2d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/jest.config.ts b/jest.config.ts index bd18033e..9b51dc89 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -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', }, diff --git a/package.json b/package.json index e596c915..393218e6 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } } diff --git a/src/InputGuesser.test.tsx b/src/InputGuesser.test.tsx index 5433cfe1..aae00aae 100644 --- a/src/InputGuesser.test.tsx +++ b/src/InputGuesser.test.tsx @@ -52,6 +52,8 @@ const dataProvider: ApiPlatformAdminDataProvider = { address: '16 avenue de Rivoli', }, ], + formatType: 'https://schema.org/EBook', + status: 'AVAILABLE', }, }), introspect: () => @@ -240,4 +242,100 @@ describe('', () => { }); }); }); + + 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( + + + + + { + updatedData = data; + }}> + + + + + + + , + ); + + // 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', + }); + }); + }, + ); }); diff --git a/src/InputGuesser.tsx b/src/InputGuesser.tsx index 1984070f..bd16c847 100644 --- a/src/InputGuesser.tsx +++ b/src/InputGuesser.tsx @@ -41,6 +41,7 @@ export const IntrospectedInputGuesser = ({ schema, schemaAnalyzer, validate, + transformEnum, ...props }: IntrospectedInputGuesserProps) => { const field = fields.find(({ name }) => name === props.source); @@ -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' ? ( + + ) : ( + + ); + } + if (['integer_id', 'id'].includes(fieldType) || field.name === 'id') { const prefix = `/${props.resource}/`; diff --git a/src/__fixtures__/parsedData.ts b/src/__fixtures__/parsedData.ts index 9671bd9d..791829e4 100644 --- a/src/__fixtures__/parsedData.ts +++ b/src/__fixtures__/parsedData.ts @@ -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, + }), ]; diff --git a/src/types.ts b/src/types.ts index 46d363bc..b10f88a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -474,12 +474,16 @@ type InputProps = | ReferenceInputProps; export type IntrospectedInputGuesserProps = Partial & - IntrospectedGuesserProps; + IntrospectedGuesserProps & { + transformEnum?: (value: string | number) => string | number; + }; export type InputGuesserProps = Omit< InputProps & Omit, 'component' ->; +> & { + transformEnum?: (value: string | number) => string | number; +}; export type IntrospecterProps = ( | CreateGuesserProps