Repository: api-platform/admin (ts)

Issue:
How to avoid using dot notation in filters input source and use as is
Hi,

I would like to use nested properties filters https://api-platform.com/docs/core/filters/#filtering-on-nested-properties
but in order to do so I need to provide querystring like this `endpoint?city.name=search`
which I cannot do because `react-admin` filter will generate `endpoint?city[name]=search` out of next example due to dot notation thing.
```js
<Filter {...props}>
    <TextInput label='City' source='`city.name`' alwaysOn />
</Filter>
```

It looks like it goes into `redux-form` and `jsx` feature set, but how do I escape dot in this case?

Cheers.
wrong syntax of query string parameters for multiple values of search filter
Hello

I'm truly a beginner in react.
I would like to use a search filter in order to searching multiple values for the same property. I saw it was possible and applied the annotation filter in my entity like this :

`@ApiFilter(SearchFilter::class, properties={"brands": "exact",})`

and wrote my react-admin filter in that way to select multiple values :

```
<Filter {...props}>
	<ReferenceArrayInput label="Services" source="brands" reference="services">
                <SelectArrayInput source="id"  optionText="name" >
	</ReferenceArrayInput>
</Filter>
```
When I pick one value, it works, however beyond one value picked, it throws a internal server error. It seems that instead of sending query string parameters like this (the right way) :

`brands[]=/services/1&brands[]=/services/2`

my request send :

`brands=/services/1,/services/2`

and throws that error :

```
@context: "/contexts/Error", @type: "hydra:Error", hydra:title: "An error occurred",…}
@context: "/contexts/Error"
@type: "hydra:Error"
hydra:description: "An exception occurred while executing 
   'SELECT DISTINCT o0_.id AS id_0, o0_.name AS name_1, o0_.id AS id_2 
    FROM organization o0_ 
    LEFT JOIN organization_service o2_ ON o0_.id = o2_.organization_id 
    LEFT JOIN service s1_ ON s1_.id = o2_.service_id 
    WHERE o0_.id IN (SELECT o3_.id FROM organization o3_ 
		LEFT JOIN organization_service o5_ ON o3_.id = o5_.organization_id 
		LEFT JOIN service s4_ ON s4_.id = o5_.service_id WHERE s4_.id = ?) 
		ORDER BY o0_.name ASC, o0_.id ASC LIMIT 30' 
		with params ["\/services\/1,\/services\/2"]:↵↵SQLSTATE[22P02]: Invalid text representation: 
		7 ERROR:  invalid input syntax for integer: "/services/1,/services/2""
hydra:title: "An error occurred"
```
Would you have any ideas to help me with that please ?
Thanks.

Pull request:
Fix nested search filters and search filter for collection
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tickets       | fixes #180, fixes #110
| License       | MIT
| Doc PR        |

Allow to traverse an infinite depth of nested search parameters and handle correctly searching in collection.

Patch:
diff --git a/src/hydra/dataProvider.js b/src/hydra/dataProvider.js
index 336f07d..02a6bfb 100644
--- a/src/hydra/dataProvider.js
+++ b/src/hydra/dataProvider.js
@@ -217,19 +217,31 @@ export default (
         if (page) collectionUrl.searchParams.set('page', page);
         if (perPage) collectionUrl.searchParams.set('perPage', perPage);
         if (params.filter) {
-          Object.keys(params.filter).forEach(key => {
-            const filterValue = params.filter[key];
+          const buildFilterParams = (key, nestedFilter, rootKey) => {
+            const filterValue = nestedFilter[key];
+
+            if (Array.isArray(filterValue)) {
+              filterValue.forEach((arrayFilterValue, index) => {
+                collectionUrl.searchParams.set(
+                  `${rootKey}[${index}]`,
+                  arrayFilterValue,
+                );
+              });
+              return;
+            }
+
             if (!isPlainObject(filterValue)) {
-              collectionUrl.searchParams.set(key, params.filter[key]);
+              collectionUrl.searchParams.set(rootKey, filterValue);
               return;
             }
 
             Object.keys(filterValue).forEach(subKey => {
-              collectionUrl.searchParams.set(
-                `${key}[${subKey}]`,
-                filterValue[subKey],
-              );
+              buildFilterParams(subKey, filterValue, `${rootKey}.${subKey}`);
             });
+          };
+
+          Object.keys(params.filter).forEach(key => {
+            buildFilterParams(key, params.filter, key);
           });
         }
 


Test patch:
diff --git a/src/hydra/dataProvider.test.js b/src/hydra/dataProvider.test.js
index d0f70a9..4bcc0c7 100644
--- a/src/hydra/dataProvider.test.js
+++ b/src/hydra/dataProvider.test.js
@@ -1,6 +1,9 @@
-import {transformJsonLdDocumentToReactAdminDocument} from './dataProvider';
+import dataProvider, {
+  transformJsonLdDocumentToReactAdminDocument,
+} from './dataProvider';
+import {GET_LIST} from 'react-admin';
 
-describe('map a json-ld document to an admin on rest compatible document', () => {
+describe('Transform a JSON-LD document to a React Admin compatible document', () => {
   const JSON_LD_DOCUMENT = {
     '@id': '/reviews/327',
     id: 327,
@@ -38,36 +41,73 @@ describe('map a json-ld document to an admin on rest compatible document', () =>
     },
   };
 
-  describe('transform the JSON-LD document in React Admin document', () => {
-    const reactAdminDocument = transformJsonLdDocumentToReactAdminDocument(
-      JSON_LD_DOCUMENT,
+  const reactAdminDocument = transformJsonLdDocumentToReactAdminDocument(
+    JSON_LD_DOCUMENT,
+  );
+
+  test('deep clone the original object', () => {
+    expect(reactAdminDocument).not.toBe(JSON_LD_DOCUMENT);
+    expect(reactAdminDocument.aNestedObject).not.toBe(
+      JSON_LD_DOCUMENT.aNestedObject,
     );
+  });
 
-    test('deep clone the original object', () => {
-      expect(reactAdminDocument).not.toBe(JSON_LD_DOCUMENT);
-      expect(reactAdminDocument.aNestedObject).not.toBe(
-        JSON_LD_DOCUMENT.aNestedObject,
-      );
-    });
+  test('add an id property equal to the original @id property', () => {
+    expect(reactAdminDocument.id).toBe(JSON_LD_DOCUMENT['@id']);
+  });
 
-    test('add an id property equal to the original @id property', () => {
-      expect(reactAdminDocument.id).toBe(JSON_LD_DOCUMENT['@id']);
-    });
+  test('preserve the previous id property value in a new originId property', () => {
+    expect(reactAdminDocument.originId).toBe(JSON_LD_DOCUMENT.id);
+  });
 
-    test('preserve the previous id property value in a new originId property', () => {
-      expect(reactAdminDocument.originId).toBe(JSON_LD_DOCUMENT.id);
-    });
+  test('an React Admin has a custom toString method', () => {
+    expect(reactAdminDocument.toString()).toBe('[object /reviews/327]');
+  });
 
-    test('an React Admin has a custom toString method', () => {
-      expect(reactAdminDocument.toString()).toBe('[object /reviews/327]');
-    });
+  test('transform embedded documents to their IRIs', () => {
+    expect(reactAdminDocument.itemReviewed).toBe('/books/2');
+  });
 
-    test('transform embedded documents to their IRIs', () => {
-      expect(reactAdminDocument.itemReviewed).toBe('/books/2');
-    });
+  test('transform arrays of embedded documents to their IRIs', () => {
+    expect(reactAdminDocument.comment[0]).toBe('/comments/1');
+  });
+});
 
-    test('transform arrays of embedded documents to their IRIs', () => {
-      expect(reactAdminDocument.comment[0]).toBe('/comments/1');
+describe('Transform a React Admin request to an Hydra request', () => {
+  const mockFetchHydra = jest.fn();
+  mockFetchHydra.mockReturnValue({
+    json: {'hydra:member': [], 'hydra:totalItems': 3},
+  });
+  const fetchApi = dataProvider('entrypoint', mockFetchHydra);
+
+  test('React Admin get list with filter parameters', () => {
+    return fetchApi(GET_LIST, 'resource', {
+      pagination: {},
+      sort: {},
+      filter: {
+        simple: 'foo',
+        nested: {param: 'bar'},
+        sub_nested: {sub: {param: true}},
+        array: ['/iri/1', '/iri/2'],
+        nested_array: {nested: ['/nested_iri/1', '/nested_iri/2']},
+      },
+    }).then(() => {
+      const searchParams = Array.from(
+        mockFetchHydra.mock.calls[0][0].searchParams.entries(),
+      );
+      expect(searchParams[0]).toEqual(['simple', 'foo']);
+      expect(searchParams[1]).toEqual(['nested.param', 'bar']);
+      expect(searchParams[2]).toEqual(['sub_nested.sub.param', 'true']);
+      expect(searchParams[3]).toEqual(['array[0]', '/iri/1']);
+      expect(searchParams[4]).toEqual(['array[1]', '/iri/2']);
+      expect(searchParams[5]).toEqual([
+        'nested_array.nested[0]',
+        '/nested_iri/1',
+      ]);
+      expect(searchParams[6]).toEqual([
+        'nested_array.nested[1]',
+        '/nested_iri/2',
+      ]);
     });
   });
 });