Skip to content

Commit

Permalink
feat: use native react-admin sanitizeEmptyValues (#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain committed Oct 4, 2022
1 parent 56aec68 commit 408d4d6
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 82 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Handle multiple file upload
* Allow to use tabbed components in guessers
* Use native react-admin `sanitizeEmptyValues`

## 3.3.8

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"jsonld": "^8.1.0",
"lodash.isplainobject": "^4.0.6",
"prop-types": "^15.6.2",
"react-admin": "^4.0.3",
"react-admin": "^4.4.0",
"react-error-boundary": "^3.1.0"
},
"devDependencies": {
Expand Down
9 changes: 3 additions & 6 deletions src/CreateGuesser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const IntrospectedCreateGuesser = ({
validate,
toolbar,
warnWhenUnsavedChanges,
sanitizeEmptyValues,
sanitizeEmptyValues = true,
formComponent,
children,
...props
Expand All @@ -71,11 +71,7 @@ export const IntrospectedCreateGuesser = ({
let inputChildren = React.Children.toArray(children);
if (inputChildren.length === 0) {
inputChildren = writableFields.map((field) => (
<InputGuesser
key={field.name}
source={field.name}
sanitizeEmptyValue={sanitizeEmptyValues}
/>
<InputGuesser key={field.name} source={field.name} />
));
displayOverrideCode(getOverrideCode(schema, writableFields));
}
Expand Down Expand Up @@ -174,6 +170,7 @@ export const IntrospectedCreateGuesser = ({
validate={validate}
toolbar={toolbar}
warnWhenUnsavedChanges={warnWhenUnsavedChanges}
sanitizeEmptyValues={sanitizeEmptyValues}
component={formComponent}>
{inputChildren}
</FormType>
Expand Down
9 changes: 3 additions & 6 deletions src/EditGuesser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const IntrospectedEditGuesser = ({
toolbar,
warnWhenUnsavedChanges,
formComponent,
sanitizeEmptyValues,
sanitizeEmptyValues = true,
children,
...props
}: IntrospectedEditGuesserProps) => {
Expand All @@ -78,11 +78,7 @@ export const IntrospectedEditGuesser = ({
let inputChildren = React.Children.toArray(children);
if (inputChildren.length === 0) {
inputChildren = writableFields.map((field) => (
<InputGuesser
key={field.name}
source={field.name}
sanitizeEmptyValue={sanitizeEmptyValues}
/>
<InputGuesser key={field.name} source={field.name} />
));
displayOverrideCode(getOverrideCode(schema, writableFields));
}
Expand Down Expand Up @@ -195,6 +191,7 @@ export const IntrospectedEditGuesser = ({
redirect={redirectTo}
toolbar={toolbar}
warnWhenUnsavedChanges={warnWhenUnsavedChanges}
sanitizeEmptyValues={sanitizeEmptyValues}
component={formComponent}>
{inputChildren}
</FormType>
Expand Down
103 changes: 96 additions & 7 deletions src/InputGuesser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ const dataProvider: ApiPlatformAdminDataProvider = {
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',
},
],
},
}),
introspect: () =>
Expand Down Expand Up @@ -102,7 +111,7 @@ describe('<InputGuesser />', () => {
});
});

test('renders a sanitized text input', async () => {
test('renders text inputs', async () => {
const user = userEvent.setup();
let updatedData = {};

Expand All @@ -113,13 +122,15 @@ describe('<InputGuesser />', () => {
<Edit id="/users/123" mutationMode="pessimistic">
<SimpleForm
onSubmit={(data: {
title?: string | null;
description?: string | null;
title?: string;
description?: string;
nullText?: string;
}) => {
updatedData = data;
}}>
<InputGuesser source="title" />
<InputGuesser source="description" sanitizeEmptyValue={false} />
<InputGuesser source="description" />
<InputGuesser source="nullText" />
</SimpleForm>
</Edit>
</ResourceContextProvider>
Expand All @@ -139,16 +150,94 @@ describe('<InputGuesser />', () => {
'resources.users.fields.description',
);
expect(descriptionField).toHaveValue('Lorem ipsum dolor sit amet');
expect(
await screen.findAllByText('resources.users.fields.nullText'),
).toHaveLength(1);
const nullTextField = screen.getByLabelText(
'resources.users.fields.nullText',
);
expect(nullTextField).toHaveValue('');

await user.clear(titleField);
expect(titleField).toHaveValue('');
await user.type(titleField, ' Foo');
expect(titleField).toHaveValue('Title Foo');
await user.clear(descriptionField);
expect(descriptionField).toHaveValue('');

const saveButton = screen.getByRole('button', { name: 'ra.action.save' });
fireEvent.click(saveButton);
await waitFor(() => {
expect(updatedData).toMatchObject({ title: null, description: '' });
expect(updatedData).toMatchObject({
title: 'Title Foo',
description: '',
nullText: '',
});
});
});

test('renders embedded inputs', async () => {
const user = userEvent.setup();
let updatedData = {};

render(
<AdminContext dataProvider={dataProvider}>
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
<ResourceContextProvider value="users">
<Edit id="/users/123" mutationMode="pessimistic">
<SimpleForm
onSubmit={(data: {
embedded?: object;
embeddeds?: object[];
}) => {
updatedData = data;
}}>
<InputGuesser source="embedded" />
<InputGuesser source="embeddeds" />
</SimpleForm>
</Edit>
</ResourceContextProvider>
</SchemaAnalyzerContext.Provider>
</AdminContext>,
);

expect(
await screen.findAllByText('resources.users.fields.embedded'),
).toHaveLength(1);
const embeddedField = screen.getByLabelText(
'resources.users.fields.embedded',
);
expect(embeddedField).toHaveValue('{"address":"91 rue du Temple"}');
expect(
await screen.findAllByText('resources.users.fields.embeddeds.0'),
).toHaveLength(1);
const embeddedsField = screen.getByLabelText(
'resources.users.fields.embeddeds.0',
);
expect(embeddedsField).toHaveValue('{"address":"16 avenue de Rivoli"}');

await user.type(embeddedField, '{ArrowLeft}, "city": "Paris"');
expect(embeddedField).toHaveValue(
'{"address":"91 rue du Temple","city":"Paris"}',
);
await user.type(embeddedsField, '{ArrowLeft}, "city": "Paris"');
expect(embeddedsField).toHaveValue(
'{"address":"16 avenue de Rivoli","city":"Paris"}',
);

const saveButton = screen.getByRole('button', { name: 'ra.action.save' });
fireEvent.click(saveButton);
await waitFor(() => {
expect(updatedData).toMatchObject({
embedded: {
address: '91 rue du Temple',
city: 'Paris',
},
embeddeds: [
{
address: '16 avenue de Rivoli',
city: 'Paris',
},
],
});
});
});
});
64 changes: 20 additions & 44 deletions src/InputGuesser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,13 @@ import type {
IntrospectedInputGuesserProps,
} from './types.js';

const convertEmptyStringToNull = (value: string) =>
value === '' ? null : value;

const convertNullToEmptyString = (value: string | null) => value ?? '';

export const IntrospectedInputGuesser = ({
fields,
readableFields,
writableFields,
schema,
schemaAnalyzer,
validate,
sanitizeEmptyValue = true,
...props
}: IntrospectedInputGuesserProps) => {
const field = fields.find(({ name }) => name === props.source);
Expand Down Expand Up @@ -107,13 +101,8 @@ export const IntrospectedInputGuesser = ({
);
}

const defaultValueSanitize = sanitizeEmptyValue ? null : '';
const formatSanitize = (value: string | null) =>
convertNullToEmptyString(value);
const parseSanitize = (value: string) =>
sanitizeEmptyValue ? convertEmptyStringToNull(value) : value;

let { format, parse } = props;
let format;
let parse;
const fieldType = schemaAnalyzer.getFieldType(field);

if (['integer_id', 'id'].includes(fieldType) || field.name === 'id') {
Expand Down Expand Up @@ -141,10 +130,7 @@ export const IntrospectedInputGuesser = ({

return JSON.stringify(value);
};
const parseEmbedded = (value: string | null) => {
if (value === null) {
return null;
}
const parseEmbedded = (value: string) => {
try {
const parsed = JSON.parse(value);
if (!isPlainObject(parsed)) {
Expand All @@ -155,24 +141,16 @@ export const IntrospectedInputGuesser = ({
return value;
}
};
const parseEmbeddedSanitize = (value: string) =>
parseEmbedded(parseSanitize(value));

if (field.embedded !== null && field.maxCardinality === 1) {
if (field.embedded !== null) {
format = formatEmbedded;
parse = parseEmbeddedSanitize;
parse = parseEmbedded;
}

let textInputFormat = formatSanitize;
let textInputParse = parseSanitize;
const { format: formatProp, parse: parseProp } = props;

switch (fieldType) {
case 'array':
if (field.embedded !== null && field.maxCardinality !== 1) {
textInputFormat = formatEmbedded;
textInputParse = parseEmbeddedSanitize;
}

return (
<ArrayInput
key={field.name}
Expand All @@ -182,9 +160,8 @@ export const IntrospectedInputGuesser = ({
<SimpleFormIterator>
<TextInput
source=""
defaultValue={defaultValueSanitize}
format={textInputFormat}
parse={textInputParse}
format={formatProp ?? format}
parse={parseProp ?? parse}
/>
</SimpleFormIterator>
</ArrayInput>
Expand All @@ -197,8 +174,8 @@ export const IntrospectedInputGuesser = ({
key={field.name}
validate={guessedValidate}
{...(props as NumberInputProps)}
format={format}
parse={parse}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand All @@ -210,8 +187,8 @@ export const IntrospectedInputGuesser = ({
step="0.1"
validate={guessedValidate}
{...(props as NumberInputProps)}
format={format}
parse={parse}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand All @@ -222,8 +199,8 @@ export const IntrospectedInputGuesser = ({
key={field.name}
validate={guessedValidate}
{...(props as BooleanInputProps)}
format={format}
parse={parse}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand All @@ -234,8 +211,8 @@ export const IntrospectedInputGuesser = ({
key={field.name}
validate={guessedValidate}
{...(props as DateInputProps)}
format={format}
parse={parse}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand All @@ -246,8 +223,8 @@ export const IntrospectedInputGuesser = ({
key={field.name}
validate={guessedValidate}
{...(props as DateTimeInputProps)}
format={format}
parse={parse}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand All @@ -257,10 +234,9 @@ export const IntrospectedInputGuesser = ({
<TextInput
key={field.name}
validate={guessedValidate}
defaultValue={defaultValueSanitize}
{...(props as TextInputProps)}
format={format ?? formatSanitize}
parse={parse ?? parseSanitize}
format={formatProp ?? format}
parse={parseProp ?? parse}
source={field.name}
/>
);
Expand Down
Loading

0 comments on commit 408d4d6

Please sign in to comment.