Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use native react-admin sanitizeEmptyValues #480

Merged
merged 1 commit into from
Oct 4, 2022
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 @@ -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