Skip to content

Commit 1e84fca

Browse files
authored
feat: add sanitizeEmptyValues prop (#460)
fix: transform prop
1 parent baf60a8 commit 1e84fca

File tree

7 files changed

+154
-22
lines changed

7 files changed

+154
-22
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 3.3.0
4+
5+
* Add `sanitizeEmptyValues` prop (default `true`) to `CreateGuesser` and `EditGuesser`
6+
* Add `sanitizeEmptyValue` prop (default `true`) to `InputGuesser`
7+
* Fix `transform` prop in `CreateGuesser` and `EditGuesser`
8+
39
## 3.2.0
410

511
* Take the operations into account (list, create, edit, delete)

src/CreateGuesser.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ export const IntrospectedCreateGuesser = ({
5050
redirect: redirectTo = 'list',
5151
mode,
5252
defaultValues,
53+
transform,
5354
validate,
5455
toolbar,
5556
warnWhenUnsavedChanges,
57+
sanitizeEmptyValues,
5658
simpleFormComponent,
5759
children,
5860
...props
@@ -66,7 +68,11 @@ export const IntrospectedCreateGuesser = ({
6668
let inputChildren = React.Children.toArray(children);
6769
if (inputChildren.length === 0) {
6870
inputChildren = writableFields.map((field) => (
69-
<InputGuesser key={field.name} source={field.name} />
71+
<InputGuesser
72+
key={field.name}
73+
source={field.name}
74+
sanitizeEmptyValue={sanitizeEmptyValues}
75+
/>
7076
));
7177
displayOverrideCode(getOverrideCode(schema, writableFields));
7278
}
@@ -78,11 +84,15 @@ export const IntrospectedCreateGuesser = ({
7884

7985
const save = useCallback(
8086
async (values: Partial<RaRecord>) => {
87+
let data = values;
88+
if (transform) {
89+
data = transform(values);
90+
}
8191
try {
8292
const response = await create(
8393
resource,
8494
{
85-
data: { ...values, extraInformation: { hasFileField } },
95+
data: { ...data, extraInformation: { hasFileField } },
8696
},
8797
{ returnPromise: true },
8898
);
@@ -136,6 +146,7 @@ export const IntrospectedCreateGuesser = ({
136146
redirect,
137147
redirectTo,
138148
schemaAnalyzer,
149+
transform,
139150
],
140151
);
141152

src/EditGuesser.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ export const IntrospectedEditGuesser = ({
5151
mode,
5252
defaultValues,
5353
validate,
54+
transform,
5455
toolbar,
5556
warnWhenUnsavedChanges,
5657
simpleFormComponent,
58+
sanitizeEmptyValues,
5759
children,
5860
...props
5961
}: IntrospectedEditGuesserProps) => {
@@ -70,7 +72,11 @@ export const IntrospectedEditGuesser = ({
7072
let inputChildren = React.Children.toArray(children);
7173
if (inputChildren.length === 0) {
7274
inputChildren = writableFields.map((field) => (
73-
<InputGuesser key={field.name} source={field.name} />
75+
<InputGuesser
76+
key={field.name}
77+
source={field.name}
78+
sanitizeEmptyValue={sanitizeEmptyValues}
79+
/>
7480
));
7581
displayOverrideCode(getOverrideCode(schema, writableFields));
7682
}
@@ -85,23 +91,27 @@ export const IntrospectedEditGuesser = ({
8591
if (id === undefined) {
8692
return undefined;
8793
}
94+
let data = values;
95+
if (transform) {
96+
data = transform(values);
97+
}
8898
try {
8999
const response = await update(
90100
resource,
91101
{
92102
id,
93-
data: { ...values, extraInformation: { hasFileField } },
103+
data: { ...data, extraInformation: { hasFileField } },
94104
},
95105
{ returnPromise: true },
96106
);
97107
const success =
98108
mutationOptions?.onSuccess ??
99-
((data: RaRecord) => {
109+
((updatedRecord: RaRecord) => {
100110
notify('ra.notification.updated', {
101111
type: 'info',
102112
messageArgs: { smart_count: 1 },
103113
});
104-
redirect(redirectTo, resource, data.id, data);
114+
redirect(redirectTo, resource, updatedRecord.id, updatedRecord);
105115
});
106116
success(response, { id, data: response, previousData: values }, {});
107117
return undefined;
@@ -149,6 +159,7 @@ export const IntrospectedEditGuesser = ({
149159
redirect,
150160
redirectTo,
151161
schemaAnalyzer,
162+
transform,
152163
],
153164
);
154165

src/InputGuesser.test.tsx

+52-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const dataProvider: ApiPlatformAdminDataProvider = {
4242
fieldB: 'fieldB value',
4343
deprecatedField: 'deprecatedField value',
4444
title: 'Title',
45-
body: 'Body',
45+
description: 'Lorem ipsum dolor sit amet',
4646
},
4747
}),
4848
introspect: () =>
@@ -92,6 +92,7 @@ describe('<InputGuesser />', () => {
9292
expect(idField).toHaveValue(123);
9393

9494
await user.type(idField, '4');
95+
await user.tab();
9596
expect(idField).toHaveValue(1234);
9697

9798
const saveButton = screen.getByRole('button', { name: 'ra.action.save' });
@@ -100,4 +101,54 @@ describe('<InputGuesser />', () => {
100101
expect(updatedData).toMatchObject({ id: 1234 });
101102
});
102103
});
104+
105+
test('renders a sanitized text input', async () => {
106+
const user = userEvent.setup();
107+
let updatedData = {};
108+
109+
render(
110+
<AdminContext dataProvider={dataProvider}>
111+
<SchemaAnalyzerContext.Provider value={hydraSchemaAnalyzer}>
112+
<ResourceContextProvider value="users">
113+
<Edit id="/users/123" mutationMode="pessimistic">
114+
<SimpleForm
115+
onSubmit={(data: {
116+
title?: string | null;
117+
description?: string | null;
118+
}) => {
119+
updatedData = data;
120+
}}>
121+
<InputGuesser source="title" />
122+
<InputGuesser source="description" sanitizeEmptyValue={false} />
123+
</SimpleForm>
124+
</Edit>
125+
</ResourceContextProvider>
126+
</SchemaAnalyzerContext.Provider>
127+
</AdminContext>,
128+
);
129+
130+
expect(
131+
await screen.findAllByText('resources.users.fields.title'),
132+
).toHaveLength(1);
133+
const titleField = screen.getByLabelText('resources.users.fields.title');
134+
expect(titleField).toHaveValue('Title');
135+
expect(
136+
await screen.findAllByText('resources.users.fields.description'),
137+
).toHaveLength(1);
138+
const descriptionField = screen.getByLabelText(
139+
'resources.users.fields.description',
140+
);
141+
expect(descriptionField).toHaveValue('Lorem ipsum dolor sit amet');
142+
143+
await user.clear(titleField);
144+
expect(titleField).toHaveValue('');
145+
await user.clear(descriptionField);
146+
expect(descriptionField).toHaveValue('');
147+
148+
const saveButton = screen.getByRole('button', { name: 'ra.action.save' });
149+
fireEvent.click(saveButton);
150+
await waitFor(() => {
151+
expect(updatedData).toMatchObject({ title: null, description: '' });
152+
});
153+
});
103154
});

src/InputGuesser.tsx

+36-9
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@ import isPlainObject from 'lodash.isplainobject';
2929
import Introspecter from './Introspecter';
3030
import type { InputGuesserProps, IntrospectedInputGuesserProps } from './types';
3131

32+
const convertEmptyStringToNull = (value: string) =>
33+
value === '' ? null : value;
34+
35+
const convertNullToEmptyString = (value: string | null) => value ?? '';
36+
3237
export const IntrospectedInputGuesser = ({
3338
fields,
3439
readableFields,
3540
writableFields,
3641
schema,
3742
schemaAnalyzer,
3843
validate,
44+
sanitizeEmptyValue = true,
3945
...props
4046
}: IntrospectedInputGuesserProps) => {
4147
const field = fields.find(({ name }) => name === props.source);
@@ -82,6 +88,12 @@ export const IntrospectedInputGuesser = ({
8288
);
8389
}
8490

91+
const defaultValueSanitize = sanitizeEmptyValue ? null : '';
92+
const formatSanitize = (value: string | null) =>
93+
convertNullToEmptyString(value);
94+
const parseSanitize = (value: string) =>
95+
sanitizeEmptyValue ? convertEmptyStringToNull(value) : value;
96+
8597
let { format, parse } = props;
8698
const fieldType = schemaAnalyzer.getFieldType(field);
8799

@@ -100,9 +112,20 @@ export const IntrospectedInputGuesser = ({
100112
};
101113
}
102114

103-
const formatEmbedded = (value: string | object) =>
104-
typeof value === 'string' ? value : JSON.stringify(value);
105-
const parseEmbedded = (value: string) => {
115+
const formatEmbedded = (value: string | object | null) => {
116+
if (value === null) {
117+
return '';
118+
}
119+
if (typeof value === 'string') {
120+
return value;
121+
}
122+
123+
return JSON.stringify(value);
124+
};
125+
const parseEmbedded = (value: string | null) => {
126+
if (value === null) {
127+
return null;
128+
}
106129
try {
107130
const parsed = JSON.parse(value);
108131
if (!isPlainObject(parsed)) {
@@ -113,20 +136,22 @@ export const IntrospectedInputGuesser = ({
113136
return value;
114137
}
115138
};
139+
const parseEmbeddedSanitize = (value: string) =>
140+
parseEmbedded(parseSanitize(value));
116141

117142
if (field.embedded !== null && field.maxCardinality === 1) {
118143
format = formatEmbedded;
119-
parse = parseEmbedded;
144+
parse = parseEmbeddedSanitize;
120145
}
121146

122-
let textInputFormat = (value: string) => value;
123-
let textInputParse = (value: string) => value;
147+
let textInputFormat = formatSanitize;
148+
let textInputParse = parseSanitize;
124149

125150
switch (fieldType) {
126151
case 'array':
127152
if (field.embedded !== null && field.maxCardinality !== 1) {
128153
textInputFormat = formatEmbedded;
129-
textInputParse = parseEmbedded;
154+
textInputParse = parseEmbeddedSanitize;
130155
}
131156

132157
return (
@@ -138,6 +163,7 @@ export const IntrospectedInputGuesser = ({
138163
<SimpleFormIterator>
139164
<TextInput
140165
source=""
166+
defaultValue={defaultValueSanitize}
141167
format={textInputFormat}
142168
parse={textInputParse}
143169
/>
@@ -212,9 +238,10 @@ export const IntrospectedInputGuesser = ({
212238
<TextInput
213239
key={field.name}
214240
validate={guessedValidate}
241+
defaultValue={defaultValueSanitize}
215242
{...(props as TextInputProps)}
216-
format={format}
217-
parse={parse}
243+
format={format ?? formatSanitize}
244+
parse={parse ?? parseSanitize}
218245
source={field.name}
219246
/>
220247
);

src/__fixtures__/parsedData.ts

+14
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,18 @@ export const API_FIELDS_DATA = [
4343
required: true,
4444
deprecated: true,
4545
}),
46+
new Field('title', {
47+
id: 'http://schema.org/title',
48+
range: 'http://www.w3.org/2001/XMLSchema#string',
49+
reference: null,
50+
embedded: null,
51+
required: false,
52+
}),
53+
new Field('description', {
54+
id: 'http://schema.org/description',
55+
range: 'http://www.w3.org/2001/XMLSchema#string',
56+
reference: null,
57+
embedded: null,
58+
required: false,
59+
}),
4660
];

src/types.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -372,25 +372,33 @@ type CreateSimpleFormProps = Omit<
372372
};
373373

374374
export type IntrospectedCreateGuesserProps = CreateSimpleFormProps &
375-
IntrospectedGuesserProps;
375+
IntrospectedGuesserProps & {
376+
sanitizeEmptyValues?: boolean;
377+
};
376378

377379
export type CreateGuesserProps = Omit<
378380
CreateSimpleFormProps & Omit<BaseIntrospecterProps, 'resource'>,
379381
'component'
380-
>;
382+
> & {
383+
sanitizeEmptyValues?: boolean;
384+
};
381385

382386
type EditSimpleFormProps = Omit<EditProps & SimpleFormProps, 'children'> &
383387
Partial<PickRename<SimpleFormProps, 'component', 'simpleFormComponent'>> & {
384388
children?: ReactNode;
385389
};
386390

387391
export type IntrospectedEditGuesserProps = EditSimpleFormProps &
388-
IntrospectedGuesserProps;
392+
IntrospectedGuesserProps & {
393+
sanitizeEmptyValues?: boolean;
394+
};
389395

390396
export type EditGuesserProps = Omit<
391397
EditSimpleFormProps & Omit<BaseIntrospecterProps, 'resource'>,
392398
'component'
393-
>;
399+
> & {
400+
sanitizeEmptyValues?: boolean;
401+
};
394402

395403
type ListDatagridProps = Omit<
396404
ListProps & Omit<DatagridProps, 'sx'>,
@@ -461,12 +469,16 @@ type InputProps =
461469
| ReferenceInputProps;
462470

463471
export type IntrospectedInputGuesserProps = Partial<InputProps> &
464-
IntrospectedGuesserProps;
472+
IntrospectedGuesserProps & {
473+
sanitizeEmptyValue?: boolean;
474+
};
465475

466476
export type InputGuesserProps = Omit<
467477
InputProps & Omit<BaseIntrospecterProps, 'resource'>,
468478
'component'
469-
>;
479+
> & {
480+
sanitizeEmptyValue?: boolean;
481+
};
470482

471483
export type IntrospecterProps = (
472484
| CreateGuesserProps

0 commit comments

Comments
 (0)