Repository: api-platform/admin
Pull request #130 (2018-11-08 13:03:32)

[RFC] Provide override functions
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | Will do after discussing

This PR is about providing some dev-friendly functions to make overriding cleaner.

Here is an example of how these functions could be used : 

_App.js_ : 
```
import React from 'react';
import { HydraAdmin} from '@api-platform/admin';
import overrideResources from '@api-platform/admin/lib/override/functions';
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';
import newResources from './override/example';

const entrypoint = process.env.REACT_APP_API_ENTRYPOINT;

const myApiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
  .then(({ api }) => {
    overrideResources(api.resources, newResources);
    return { api };
  })
;

export default () => <HydraAdmin apiDocumentationParser={myApiDocumentationParser} entrypoint={entrypoint} />;
```

So, now the developer has to provide the `newResources` : 
```import React from 'react';
import {TextInput, UrlField} from 'react-admin';
import BookList from '../books/list-1';
import BookCreate from '../books/create-2';
import BookEdit from '../books/edit-1';

const greetingsNameInput = props => (
  <TextInput label={'POC'} {...props} />
);

const greetingsNameField = props => (
  <UrlField {...props} />
);

const booksDescriptionInput = props => (
  <TextInput label={'POC'} {...props} />
);

const greetingsOverride = {
  name: 'greetings',
  fields: [
    {
      name: 'name',
      input: greetingsNameInput,
      field: greetingsNameField,
    }
  ],
  listFields: [],
};

const booksOverride = {
  name: 'books',
  list: BookList,
  create: BookCreate,
  edit: BookEdit,
  fields: [
    {
      name: 'description',
      input: booksDescriptionInput,
    }
  ]
};

export default [
  greetingsOverride,
  booksOverride,
];
```

And here are some examples of what overriding operations could look like : 

A Create : 

```
import React, {Component} from 'react';
import {Create, SimpleForm} from 'react-admin';
import {getResourceField} from '@api-platform/admin/src/override/functions';

/* Rearrange fields : order or div containers */

class BookCreate extends Component {
  render() {
    const {options: {inputFactory, resource}} = this.props;

    return (
      <Create {...this.props}>
        <SimpleForm>
          <div className="row">
            <div className="col-md-6">
              {inputFactory(getResourceField(resource, 'name'))}
            </div>
            <div className="col-md-6">
              {inputFactory(getResourceField(resource, 'description'))}
            </div>
          </div>
        </SimpleForm>
      </Create>
    );
  }
}

export default BookCreate;
```

And an Edit : 

```
import React, {Component} from 'react';
import {Edit, SimpleForm} from 'react-admin';
import {connect} from 'react-redux';
import {formValueSelector} from 'redux-form';
import {getResourceField} from '@api-platform/admin/src/override/functions';

/* Dynamic display */

class BookEdit extends Component {
  render() {
    const {options: {inputFactory, resource}, formValueName} = this.props;

    return (
      <Edit {...this.props}>
        <SimpleForm>

          {inputFactory(getResourceField(resource, 'name'))}

          {formValueName && (
            inputFactory(getResourceField(resource, 'description'))
          )}

        </SimpleForm>
      </Edit>
    );
  }
}

const mapStateToProps = state => ({
  formValueName: formValueSelector('record-form')(state, 'name'),
});

export default connect(mapStateToProps)(BookEdit);
```

Do you think there is any point of include these functions into API-P or this is something that should be done inside the project using API-P ?

dunglas: I like the idea!

Patch:
diff --git a/package.json b/package.json
index 0ccd518..71f896a 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "react-test-renderer": "~16.3.1"
   },
   "dependencies": {
-    "@api-platform/api-doc-parser": "^0.4",
+    "@api-platform/api-doc-parser": "^0.6",
     "@babel/runtime": "7.0.0-beta.55",
     "jsonld": "^0.4",
     "lodash.isplainobject": "^4.0.6",
diff --git a/src/AdminBuilder.js b/src/AdminBuilder.js
index 60fa59e..904cbd2 100644
--- a/src/AdminBuilder.js
+++ b/src/AdminBuilder.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import fieldFactory from './fieldFactory';
 import inputFactory from './inputFactory';
+import parameterFactory from './parameterFactory';
 import resourceFactory from './resourceFactory';
 
 const AdminBuilder = props => {
@@ -12,6 +13,7 @@ const AdminBuilder = props => {
     fieldFactory,
     inputFactory,
     resourceFactory,
+    parameterFactory,
     title = api.title,
     resources = api.resources.filter(({deprecated}) => !deprecated),
   } = props;
@@ -19,7 +21,13 @@ const AdminBuilder = props => {
   return (
     <Admin {...props} title={title}>
       {resources.map(resource =>
-        resourceFactory(resource, api, fieldFactory, inputFactory),
+        resourceFactory(
+          resource,
+          api,
+          fieldFactory,
+          inputFactory,
+          parameterFactory,
+        ),
       )}
     </Admin>
   );
@@ -29,12 +37,14 @@ AdminBuilder.defaultProps = {
   fieldFactory,
   inputFactory,
   resourceFactory,
+  parameterFactory,
 };
 
 AdminBuilder.propTypes = {
   api: PropTypes.instanceOf(Api).isRequired,
   fieldFactory: PropTypes.func,
   inputFactory: PropTypes.func,
+  parameterFactory: PropTypes.func,
   resourceFactory: PropTypes.func,
   dataProvider: PropTypes.func.isRequired,
   resource: PropTypes.array,
diff --git a/src/List.js b/src/List.js
index ee06cbf..f68b533 100644
--- a/src/List.js
+++ b/src/List.js
@@ -9,6 +9,8 @@ import {
 } from 'react-admin';
 import PropTypes from 'prop-types';
 import React from 'react';
+import ListFilter from './ListFilter';
+import {isFieldSortable} from './fieldFactory';
 
 const hasIdentifier = fields => {
   return (
@@ -18,7 +20,11 @@ const hasIdentifier = fields => {
 
 const resolveProps = props => {
   const {options} = props;
-  const {fieldFactory: defaultFieldFactory, resource} = options;
+  const {
+    fieldFactory: defaultFieldFactory,
+    parameterFactory,
+    resource,
+  } = options;
   const {
     listFields: customFields,
     listProps = {},
@@ -34,6 +40,8 @@ const resolveProps = props => {
       fields:
         customFields || defaultFields.filter(({deprecated}) => !deprecated),
       fieldFactory: customFieldFactory || defaultFieldFactory,
+      parameterFactory: parameterFactory,
+      parameters: resource.parameters,
     },
   };
 };
@@ -42,14 +50,28 @@ const List = props => {
   const {
     hasEdit,
     hasShow,
-    options: {api, fieldFactory, fields, resource},
+    options: {
+      api,
+      fieldFactory,
+      fields,
+      parameterFactory,
+      parameters,
+      resource,
+    },
     addIdField = false === hasIdentifier(fields),
   } = resolveProps(props);
 
   return (
-    <BaseList {...props}>
+    <BaseList
+      {...props}
+      filters={<ListFilter options={{parameterFactory, parameters}} />}>
       <Datagrid>
-        {addIdField && <TextField source="id" />}
+        {addIdField && (
+          <TextField
+            source="id"
+            sortable={isFieldSortable({name: 'id'}, resource)}
+          />
+        )}
         {fields.map(field =>
           fieldFactory(field, {
             api,
@@ -72,6 +94,7 @@ List.propTypes = {
   options: PropTypes.shape({
     api: PropTypes.instanceOf(Api).isRequired,
     fieldFactory: PropTypes.func.isRequired,
+    parameterFactory: PropTypes.func.isRequired,
     listProps: PropTypes.object,
     resource: PropTypes.instanceOf(Resource).isRequired,
   }),
diff --git a/src/ListFilter.js b/src/ListFilter.js
new file mode 100644
index 0000000..1e437a0
--- /dev/null
+++ b/src/ListFilter.js
@@ -0,0 +1,31 @@
+import {Filter} from 'react-admin';
+import React from 'react';
+
+const resolveProps = props => {
+  const {options} = props;
+  const {parameterFactory, parameters} = options;
+  return {
+    ...props,
+    parameterFactory: parameterFactory,
+    parameters: parameters,
+  };
+};
+
+const ListFilter = props => {
+  const {parameters, parameterFactory} = resolveProps(props);
+
+  const parameterAlwaysOn = parameters.length < 8;
+
+  return (
+    <Filter {...props}>
+      {parameters.length > 0 &&
+        parameters.map(parameter =>
+          parameterFactory(parameter, {
+            alwaysOn: parameterAlwaysOn,
+          }),
+        )}
+    </Filter>
+  );
+};
+
+export default ListFilter;
diff --git a/src/docsUtils/index.js b/src/docsUtils/index.js
new file mode 100644
index 0000000..58ccfd0
--- /dev/null
+++ b/src/docsUtils/index.js
@@ -0,0 +1,61 @@
+export function getResource(resources, resourceName) {
+  return resources.find(({name}) => resourceName === name);
+}
+
+export function getResourceField(resource, resourceFieldName) {
+  return resource.fields.find(({name}) => resourceFieldName === name);
+}
+
+export function replaceInput(resource, resourceFieldName, replaceInput) {
+  const resourceField = getResourceField(resource, resourceFieldName);
+
+  if (undefined === resourceField) return;
+
+  resourceField.input = replaceInput;
+}
+
+export function replaceField(resource, resourceFieldName, replaceField) {
+  const resourceField = getResourceField(resource, resourceFieldName);
+
+  if (undefined === resourceField) return;
+
+  resourceField.field = replaceField;
+}
+
+export function replaceResource(resources, replaceResource) {
+  const resource = getResource(resources, replaceResource.name);
+
+  if (undefined !== replaceResource.listFields) {
+    resource.listFields = replaceResource.listFields;
+  }
+
+  if (undefined !== replaceResource.fields) {
+    replaceResource.fields.forEach(resourceField => {
+      if (resourceField.field) {
+        replaceField(resource, resourceField.name, resourceField.field);
+      }
+
+      if (resourceField.input) {
+        replaceInput(resource, resourceField.name, resourceField.input);
+      }
+    });
+  }
+
+  if (undefined !== replaceResource.list) {
+    resource.list = replaceResource.list;
+  }
+
+  if (undefined !== replaceResource.create) {
+    resource.create = replaceResource.create;
+  }
+
+  if (undefined !== replaceResource.edit) {
+    resource.edit = replaceResource.edit;
+  }
+}
+
+export function replaceResources(resources, replaceResources) {
+  replaceResources.forEach(resource => {
+    replaceResource(resources, resource);
+  });
+}
diff --git a/src/fieldFactory.js b/src/fieldFactory.js
index 00a0bc1..96c117d 100644
--- a/src/fieldFactory.js
+++ b/src/fieldFactory.js
@@ -13,6 +13,16 @@ import {
 import React from 'react';
 import getReferenceNameField from './getReferenceNameField';
 
+export const isFieldSortable = (field, resource) => {
+  return (
+    resource.parameters.filter(parameter => parameter.variable === field.name)
+      .length > 0 &&
+    resource.parameters.filter(
+      parameter => parameter.variable === `order[${field.name}]`,
+    ).length > 0
+  );
+};
+
 export default (field, options) => {
   const props = {...field.fieldProps};
   if (field.field) {
@@ -33,6 +43,7 @@ export default (field, options) => {
           source={field.name}
           reference={field.reference.name}
           key={field.name}
+          sortable={isFieldSortable(field, options.resource)}
           {...props}
           allowEmpty>
           <ChipField source={getReferenceNameField(field.reference)} />
@@ -46,6 +57,7 @@ export default (field, options) => {
         source={field.name}
         reference={field.reference.name}
         key={field.name}
+        sortable={isFieldSortable(field, options.resource)}
         {...props}>
         <SingleFieldList>
           <ChipField source={referenceNameField} key={referenceNameField} />
@@ -56,10 +68,24 @@ export default (field, options) => {
 
   switch (field.id) {
     case 'http://schema.org/email':
-      return <EmailField key={field.name} source={field.name} {...props} />;
+      return (
+        <EmailField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
 
     case 'http://schema.org/url':
-      return <UrlField key={field.name} source={field.name} {...props} />;
+      return (
+        <UrlField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
 
     default:
     // Do nothing
@@ -68,16 +94,44 @@ export default (field, options) => {
   switch (field.range) {
     case 'http://www.w3.org/2001/XMLSchema#integer':
     case 'http://www.w3.org/2001/XMLSchema#float':
-      return <NumberField key={field.name} source={field.name} {...props} />;
+      return (
+        <NumberField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
 
     case 'http://www.w3.org/2001/XMLSchema#date':
     case 'http://www.w3.org/2001/XMLSchema#dateTime':
-      return <DateField key={field.name} source={field.name} {...props} />;
+      return (
+        <DateField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
 
     case 'http://www.w3.org/2001/XMLSchema#boolean':
-      return <BooleanField key={field.name} source={field.name} {...props} />;
+      return (
+        <BooleanField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
 
     default:
-      return <TextField key={field.name} source={field.name} {...props} />;
+      return (
+        <TextField
+          key={field.name}
+          source={field.name}
+          sortable={isFieldSortable(field, options.resource)}
+          {...props}
+        />
+      );
   }
 };
diff --git a/src/hydra/hydraClient.js b/src/hydra/hydraClient.js
index 0593ca7..ef5cf53 100644
--- a/src/hydra/hydraClient.js
+++ b/src/hydra/hydraClient.js
@@ -168,8 +168,9 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
    * @returns {Object}
    */
   const convertReactAdminRequestToHydraRequest = (type, resource, params) => {
-    const collectionUrl = new URL(`${entrypoint}/${resource}`);
-    const itemUrl = new URL(params.id, entrypoint);
+    const entrypointUrl = new URL(entrypoint, window.location.href);
+    const collectionUrl = new URL(`${entrypoint}/${resource}`, entrypointUrl);
+    const itemUrl = new URL(params.id, entrypointUrl);
 
     switch (type) {
       case CREATE:
@@ -201,9 +202,20 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
         if (page) collectionUrl.searchParams.set('page', page);
         if (perPage) collectionUrl.searchParams.set('perPage', perPage);
         if (params.filter) {
-          Object.keys(params.filter).map(key =>
-            collectionUrl.searchParams.set(key, params.filter[key]),
-          );
+          Object.keys(params.filter).forEach(key => {
+            const filterValue = params.filter[key];
+            if (!isPlainObject(filterValue)) {
+              collectionUrl.searchParams.set(key, params.filter[key]);
+              return;
+            }
+
+            Object.keys(filterValue).forEach(subKey => {
+              collectionUrl.searchParams.set(
+                `${key}[${subKey}]`,
+                filterValue[subKey],
+              );
+            });
+          });
         }
 
         return Promise.resolve({
diff --git a/src/index.js b/src/index.js
index dc611c2..f8f2834 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,3 +7,4 @@ export resourceFactory from './resourceFactory';
 export List from './List';
 export Show from './Show';
 export * from './hydra';
+export * from './docsUtils';
diff --git a/src/inputFactory.js b/src/inputFactory.js
index db020a4..2eddefb 100644
--- a/src/inputFactory.js
+++ b/src/inputFactory.js
@@ -1,4 +1,5 @@
 import {
+  ArrayInput,
   BooleanInput,
   DateInput,
   NumberInput,
@@ -7,6 +8,7 @@ import {
   required,
   SelectArrayInput,
   SelectInput,
+  SimpleFormIterator,
   TextInput,
 } from 'react-admin';
 import React from 'react';
@@ -72,6 +74,15 @@ export default (field, options) => {
   }
 
   switch (field.range) {
+    case 'http://www.w3.org/2001/XMLSchema#array':
+      return (
+        <ArrayInput key={field.name} source={field.name} {...props}>
+          <SimpleFormIterator>
+            <TextInput />
+          </SimpleFormIterator>
+        </ArrayInput>
+      );
+
     case 'http://www.w3.org/2001/XMLSchema#integer':
       return <NumberInput key={field.name} source={field.name} {...props} />;
 
diff --git a/src/parameterFactory.js b/src/parameterFactory.js
new file mode 100644
index 0000000..a5f9033
--- /dev/null
+++ b/src/parameterFactory.js
@@ -0,0 +1,85 @@
+import {
+  DateInput,
+  NullableBooleanInput,
+  NumberInput,
+  TextInput,
+} from 'react-admin';
+import React from 'react';
+
+function guessType(parameter, apiPlatform) {
+  let type = guessTypeForApiPlatform(parameter, apiPlatform);
+
+  return type !== undefined ? type : guessTypeFromRange(parameter.range);
+}
+
+function guessTypeForApiPlatform(parameter, apiPlatform = true) {
+  if (apiPlatform) {
+    // List filters are discarded because there is no built-in filter component in react-admin that can handle this case.
+    if (parameter.variable.match(/.*\[\]/i)) return null;
+    // Order filters are discaded because it is only used to know if a column should be sortable or not.
+    if (parameter.variable.match(/^order\[.+\]$/)) return null;
+    if (parameter.variable.match(/.*\[between\]/i) !== null) return 'between';
+  }
+}
+
+function guessTypeFromRange(range) {
+  switch (range) {
+    case 'http://www.w3.org/2001/XMLSchema#integer':
+    case 'http://www.w3.org/2001/XMLSchema#float':
+      return 'number';
+    case 'http://www.w3.org/2001/XMLSchema#boolean':
+      return 'nullableBoolean';
+    case 'http://www.w3.org/2001/XMLSchema#dateTime':
+      return 'date';
+    default:
+      return 'text';
+  }
+}
+
+export default (parameter, options) => {
+  let type = guessType(parameter, options.apiPlatform);
+
+  switch (type) {
+    case 'date':
+      return (
+        <DateInput
+          key={parameter.variable}
+          source={parameter.variable}
+          {...options}
+        />
+      );
+
+    case 'nullableBoolean':
+      return (
+        <NullableBooleanInput
+          key={parameter.variable}
+          source={parameter.variable}
+          {...options}
+        />
+      );
+
+    // TODO : create a dedicated Input
+    case 'between':
+      return null;
+
+    case 'number':
+      return (
+        <NumberInput
+          key={parameter.variable}
+          source={parameter.variable}
+          {...options}
+        />
+      );
+
+    case 'text':
+      return (
+        <TextInput
+          key={parameter.variable}
+          source={parameter.variable}
+          {...options}
+        />
+      );
+
+    default:
+  }
+};
diff --git a/src/resourceFactory.js b/src/resourceFactory.js
index 815dde8..9783be1 100644
--- a/src/resourceFactory.js
+++ b/src/resourceFactory.js
@@ -5,7 +5,13 @@ import Edit from './Edit';
 import List from './List';
 import Show from './Show';
 
-export default (resource, api, fieldFactory, inputFactory) => {
+export default (
+  resource,
+  api,
+  fieldFactory,
+  inputFactory,
+  parameterFactory,
+) => {
   const {
     create = Create,
     edit = Edit,
@@ -26,6 +32,7 @@ export default (resource, api, fieldFactory, inputFactory) => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
         resource,
       }}
       show={show}
diff --git a/yarn.lock b/yarn.lock
index 4cd8b0f..7540836 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,10 +2,10 @@
 # yarn lockfile v1
 
 
-"@api-platform/api-doc-parser@^0.4":
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/@api-platform/api-doc-parser/-/api-doc-parser-0.4.0.tgz#92f24a445aaa2a8fac78e3a2069a78414854e9bb"
-  integrity sha512-ZCSktutVrZaG3+QrKRQ0i71oT5ag8SeHzpTLEW+uIo79Zp9FJpC77fe2YvUbGhbOWM+epjKUZRr+Kit2DYoN1Q==
+"@api-platform/api-doc-parser@^0.6":
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/@api-platform/api-doc-parser/-/api-doc-parser-0.6.1.tgz#953fb37947eb760c1a8e5d891de2ddbd706a0bbd"
+  integrity sha512-/e3WhJyEnISxC7zv5udpFNsx6P5i8DhW+8gUy8bxVmYrbM7fCCKJvEqcYF/EFirfbB3qsFDCztem73VgP1V3HA==
   dependencies:
     babel-runtime "^6.23.0"
     jsonld "^0.4.11"
@@ -166,15 +166,15 @@ ajv@^4.7.0:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^5.3.0:
-  version "5.5.2"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
-  integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=
+ajv@^6.5.5:
+  version "6.5.5"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
+  integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
   dependencies:
-    co "^4.6.0"
-    fast-deep-equal "^1.0.0"
+    fast-deep-equal "^2.0.1"
     fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.3.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
 
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
@@ -1575,10 +1575,10 @@ color-name@1.1.1:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
   integrity sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=
 
-combined-stream@1.0.6, combined-stream@~1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
-  integrity sha1-cj599ugBrFYTETp+RFqbactjKBg=
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+  integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
   dependencies:
     delayed-stream "~1.0.0"
 
@@ -2373,10 +2373,10 @@ extsprintf@^1.2.0:
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
 
-fast-deep-equal@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
-  integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=
+fast-deep-equal@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+  integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
 
 fast-diff@^1.1.1:
   version "1.1.2"
@@ -2513,12 +2513,12 @@ forever-agent@~0.6.1:
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
 form-data@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
-  integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
   dependencies:
     asynckit "^0.4.0"
-    combined-stream "1.0.6"
+    combined-stream "^1.0.6"
     mime-types "^2.1.12"
 
 fragment-cache@^0.2.1:
@@ -2691,11 +2691,11 @@ har-schema@^2.0.0:
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
 har-validator@~5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
-  integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
   dependencies:
-    ajv "^5.3.0"
+    ajv "^6.5.5"
     har-schema "^2.0.0"
 
 has-ansi@^2.0.0:
@@ -3701,10 +3701,10 @@ jsesc@~0.5.0:
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
-json-schema-traverse@^0.3.0:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
-  integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
 json-schema@0.2.3:
   version "0.2.3"
@@ -4136,17 +4136,17 @@ micromatch@^3.1.4, micromatch@^3.1.8:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-mime-db@~1.35.0:
-  version "1.35.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
-  integrity sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==
+mime-db@~1.37.0:
+  version "1.37.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
+  integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
 
 mime-types@^2.1.12, mime-types@~2.1.19:
-  version "2.1.19"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
-  integrity sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==
+  version "2.1.21"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
+  integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
   dependencies:
-    mime-db "~1.35.0"
+    mime-db "~1.37.0"
 
 mimic-fn@^1.0.0:
   version "1.2.0"
@@ -5689,19 +5689,18 @@ sprintf-js@~1.0.2:
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
 sshpk@^1.7.0:
-  version "1.14.2"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
-  integrity sha1-xvxhZIo9nE52T9P8306hBeSSupg=
+  version "1.15.2"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
+  integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
-    dashdash "^1.12.0"
-    getpass "^0.1.1"
-    safer-buffer "^2.0.2"
-  optionalDependencies:
     bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
     ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
     jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
     tweetnacl "~0.14.0"
 
 stack-utils@^1.0.1:
@@ -6016,6 +6015,13 @@ unset-value@^1.0.0:
     has-value "^0.3.1"
     isobject "^3.0.0"
 
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
 urix@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"


Test patch:
diff --git a/src/Create.test.js b/src/Create.test.js
index def7b8c..e744a59 100644
--- a/src/Create.test.js
+++ b/src/Create.test.js
@@ -1,7 +1,7 @@
 import Api from '@api-platform/api-doc-parser/lib/Api';
 import Field from '@api-platform/api-doc-parser/lib/Field';
 import Resource from '@api-platform/api-doc-parser/lib/Resource';
-import {TextInput} from 'react-admin';
+import {ArrayInput, SimpleFormIterator, TextInput} from 'react-admin';
 import {shallow} from 'enzyme';
 import React from 'react';
 import Create from './Create';
@@ -36,6 +36,12 @@ const resourceData = {
       required: true,
       deprecated: true,
     }),
+    new Field('fieldD', {
+      id: 'http://schema.org/fieldD',
+      range: 'http://www.w3.org/2001/XMLSchema#array',
+      reference: null,
+      required: true,
+    }),
   ],
 };
 
@@ -67,8 +73,11 @@ describe('<Create />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
-    expect(render.find(TextInput).length).toEqual(2);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(ArrayInput).length).toEqual(1);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
   });
@@ -104,9 +113,12 @@ describe('<Create />', () => {
       />,
     );
 
-    expect(customInputFactory).toHaveBeenCalledTimes(2);
+    expect(customInputFactory).toHaveBeenCalledTimes(3);
     expect(defaultInputFactory).toHaveBeenCalledTimes(0);
-    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(ArrayInput).length).toEqual(1);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
   });
@@ -123,6 +135,12 @@ describe('<Create />', () => {
           reference: null,
           required: true,
         }),
+        new Field('fieldE', {
+          id: 'http://schema.org/fieldE',
+          range: 'http://www.w3.org/2001/XMLSchema#array',
+          reference: null,
+          required: true,
+        }),
       ],
     });
 
@@ -144,8 +162,11 @@ describe('<Create />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(1);
-    expect(render.find(TextInput).length).toEqual(1);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    expect(render.find(ArrayInput).length).toEqual(1);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldE');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldC');
   });
 });
diff --git a/src/Edit.test.js b/src/Edit.test.js
index f46218c..77d7980 100644
--- a/src/Edit.test.js
+++ b/src/Edit.test.js
@@ -1,7 +1,12 @@
 import Api from '@api-platform/api-doc-parser/lib/Api';
 import Field from '@api-platform/api-doc-parser/lib/Field';
 import Resource from '@api-platform/api-doc-parser/lib/Resource';
-import {DisabledInput, TextInput} from 'react-admin';
+import {
+  ArrayInput,
+  DisabledInput,
+  SimpleFormIterator,
+  TextInput,
+} from 'react-admin';
 import {shallow} from 'enzyme';
 import React from 'react';
 import Edit from './Edit';
@@ -36,6 +41,12 @@ const resourceData = {
       required: true,
       deprecated: true,
     }),
+    new Field('fieldD', {
+      id: 'http://schema.org/fieldD',
+      range: 'http://www.w3.org/2001/XMLSchema#array',
+      reference: null,
+      required: true,
+    }),
   ],
 };
 
@@ -67,10 +78,13 @@ describe('<Edit />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(ArrayInput).length).toEqual(1);
     expect(render.find(DisabledInput).length).toEqual(1);
     expect(render.find(DisabledInput).get(0).props.source).toEqual('id');
-    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
   });
@@ -103,9 +117,11 @@ describe('<Edit />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(ArrayInput).length).toEqual(1);
     expect(render.find(DisabledInput).length).toEqual(0);
-    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
   });
@@ -141,11 +157,14 @@ describe('<Edit />', () => {
       />,
     );
 
-    expect(customInputFactory).toHaveBeenCalledTimes(2);
+    expect(customInputFactory).toHaveBeenCalledTimes(3);
     expect(defaultInputFactory).toHaveBeenCalledTimes(0);
+    expect(render.find(ArrayInput).length).toEqual(1);
     expect(render.find(DisabledInput).length).toEqual(1);
     expect(render.find(DisabledInput).get(0).props.source).toEqual('id');
-    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
   });
@@ -162,6 +181,12 @@ describe('<Edit />', () => {
           reference: null,
           required: true,
         }),
+        new Field('fieldE', {
+          id: 'http://schema.org/fieldE',
+          range: 'http://www.w3.org/2001/XMLSchema#array',
+          reference: null,
+          required: true,
+        }),
       ],
     });
 
@@ -183,10 +208,13 @@ describe('<Edit />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(1);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    expect(render.find(ArrayInput).length).toEqual(1);
     expect(render.find(DisabledInput).length).toEqual(1);
     expect(render.find(DisabledInput).get(0).props.source).toEqual('id');
-    expect(render.find(TextInput).length).toEqual(1);
+    expect(render.find(SimpleFormIterator).length).toEqual(1);
+    expect(render.find(TextInput).length).toEqual(2);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldE');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldC');
   });
 
@@ -227,11 +255,14 @@ describe('<Edit />', () => {
       />,
     );
 
-    expect(defaultInputFactory).toHaveBeenCalledTimes(3);
+    expect(defaultInputFactory).toHaveBeenCalledTimes(4);
+    expect(render.find(ArrayInput).length).toEqual(1);
     expect(render.find(DisabledInput).length).toEqual(0);
-    expect(render.find(TextInput).length).toEqual(3);
+    expect(render.find(TextInput).length).toEqual(4);
+    expect(render.find(ArrayInput).get(0).props.source).toEqual('fieldD');
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
-    expect(render.find(TextInput).get(2).props.source).toEqual('id');
+    expect(render.find(TextInput).get(2).props.source).toEqual(undefined);
+    expect(render.find(TextInput).get(3).props.source).toEqual('id');
   });
 });
diff --git a/src/List.test.js b/src/List.test.js
index 8e14c57..9f1b689 100644
--- a/src/List.test.js
+++ b/src/List.test.js
@@ -1,10 +1,12 @@
 import Api from '@api-platform/api-doc-parser/lib/Api';
 import Field from '@api-platform/api-doc-parser/lib/Field';
+import Parameter from '@api-platform/api-doc-parser/lib/Parameter';
 import Resource from '@api-platform/api-doc-parser/lib/Resource';
 import {TextField} from 'react-admin';
 import {shallow} from 'enzyme';
 import React from 'react';
 import fieldFactory from './fieldFactory';
+import parameterFactory from './parameterFactory';
 import List from './List';
 
 const entrypoint = 'http://entrypoint';
@@ -36,12 +38,17 @@ const resourceData = {
       deprecated: true,
     }),
   ],
+  parameters: [
+    new Parameter('fieldA', '', false, ''),
+    new Parameter('order[fieldA]', '', false, ''),
+  ],
   url: `${entrypoint}/users`,
 };
 
 describe('<List />', () => {
   test('without overrides', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultParameterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(
       resourceData.name,
@@ -61,6 +68,7 @@ describe('<List />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultParameterFactory,
           resource,
         }}
         location={{}}
@@ -78,6 +86,7 @@ describe('<List />', () => {
 
   test('without default identifier field', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultParameterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -98,6 +107,7 @@ describe('<List />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultParameterFactory,
           resource,
         }}
         location={{}}
@@ -115,6 +125,7 @@ describe('<List />', () => {
   test('with custom fieldFactory', () => {
     const customFieldFactory = jest.fn(fieldFactory);
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultParameterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -137,6 +148,7 @@ describe('<List />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultParameterFactory,
           resource,
         }}
         location={{}}
@@ -155,6 +167,7 @@ describe('<List />', () => {
 
   test('with custom fields', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultParameterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -180,6 +193,7 @@ describe('<List />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultParameterFactory,
           resource,
         }}
         location={{}}
@@ -196,6 +210,7 @@ describe('<List />', () => {
 
   test('with readable identifier', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultParameterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -225,6 +240,7 @@ describe('<List />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultParameterFactory,
           resource,
         }}
         location={{}}
diff --git a/src/ListFilter.test.js b/src/ListFilter.test.js
new file mode 100644
index 0000000..1f62378
--- /dev/null
+++ b/src/ListFilter.test.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import Field from '@api-platform/api-doc-parser/lib/Field';
+import Resource from '@api-platform/api-doc-parser/lib/Resource';
+import {TextInput} from 'react-admin';
+import {shallow} from 'enzyme';
+import parameterFactory from './parameterFactory';
+import ListFilter from './ListFilter';
+import Parameter from '@api-platform/api-doc-parser/lib/Parameter';
+
+const entrypoint = 'http://entrypoint';
+
+const resourceData = {
+  name: 'user',
+  fields: [
+    new Field('fieldA', {
+      id: 'http://schema.org/fieldA',
+      range: 'http://www.w3.org/2001/XMLSchema#string',
+      reference: null,
+      required: true,
+    }),
+    new Field('fieldB', {
+      id: 'http://schema.org/fieldB',
+      range: 'http://www.w3.org/2001/XMLSchema#string',
+      reference: null,
+      required: true,
+    }),
+    new Field('deprecatedField', {
+      id: 'http://localhost/deprecatedField',
+      range: 'http://www.w3.org/2001/XMLSchema#string',
+      reference: null,
+      required: true,
+      deprecated: true,
+    }),
+  ],
+  parameters: [
+    new Parameter('fieldA', '', false, ''),
+    new Parameter('order[fieldA]', '', false, ''),
+  ],
+  url: `${entrypoint}/users`,
+};
+
+describe('<ListFilter />', () => {
+  test('without overrides', () => {
+    const defaultParameterFactory = jest.fn(parameterFactory);
+
+    const resource = new Resource(
+      resourceData.name,
+      resourceData.url,
+      resourceData,
+    );
+
+    const {parameters} = resource;
+
+    const render = shallow(
+      <ListFilter
+        options={{
+          parameters,
+          parameterFactory: defaultParameterFactory,
+        }}
+      />,
+    );
+
+    expect(defaultParameterFactory).toHaveBeenCalledTimes(2);
+    expect(render.find(TextInput).getNodes()).toHaveLength(1);
+    expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
+  });
+});
diff --git a/src/Show.test.js b/src/Show.test.js
index e7e8d2d..f038a27 100644
--- a/src/Show.test.js
+++ b/src/Show.test.js
@@ -5,6 +5,7 @@ import {TextField} from 'react-admin';
 import {shallow} from 'enzyme';
 import React from 'react';
 import fieldFactory from './fieldFactory';
+import parameterFactory from './parameterFactory';
 import Show from './Show';
 
 const entrypoint = 'http://entrypoint';
@@ -36,12 +37,14 @@ const resourceData = {
       deprecated: true,
     }),
   ],
+  parameters: [],
   url: `${entrypoint}/users`,
 };
 
 describe('<Show />', () => {
   test('without overrides', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultFilterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(
       resourceData.name,
@@ -59,6 +62,7 @@ describe('<Show />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultFilterFactory,
           resource,
         }}
         location={{}}
@@ -76,6 +80,7 @@ describe('<Show />', () => {
 
   test('without default identifier field', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultFilterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -94,6 +99,7 @@ describe('<Show />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultFilterFactory,
           resource,
         }}
         location={{}}
@@ -111,6 +117,7 @@ describe('<Show />', () => {
   test('with custom fieldFactory', () => {
     const customFieldFactory = jest.fn(fieldFactory);
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultFilterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -131,6 +138,7 @@ describe('<Show />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultFilterFactory,
           resource,
         }}
         location={{}}
@@ -149,6 +157,7 @@ describe('<Show />', () => {
 
   test('with custom fields', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultFilterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -172,6 +181,7 @@ describe('<Show />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultFilterFactory,
           resource,
         }}
         location={{}}
@@ -188,6 +198,7 @@ describe('<Show />', () => {
 
   test('with readable identifier', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
+    const defaultFilterFactory = jest.fn(parameterFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
       ...resourceData,
@@ -215,6 +226,7 @@ describe('<Show />', () => {
         options={{
           api,
           fieldFactory: defaultFieldFactory,
+          parameterFactory: defaultFilterFactory,
           resource,
         }}
         location={{}}
diff --git a/src/hydra/hydraClient.test.js b/src/hydra/hydraClient.test.js
index a436c00..a05c8e4 100644
--- a/src/hydra/hydraClient.test.js
+++ b/src/hydra/hydraClient.test.js
@@ -100,6 +100,26 @@ describe('fetch data from an hydra api', () => {
     });
   });
 
+  test('fetch a get_list resource with relative api url', async () => {
+    const mockHttpClient = jest.fn();
+    mockHttpClient.mockReturnValue(
+      Promise.resolve({
+        json: {
+          'hydra:member': [{'@id': 'books/5'}],
+        },
+      }),
+    );
+
+    await hydraClient({entrypoint: '/api'}, mockHttpClient)(GET_LIST, 'books', {
+      pagination: {page: 2},
+      sort: {field: 'id', order: 'ASC'},
+    }).then(() => {
+      expect(mockHttpClient.mock.calls[0][0].href).toBe(
+        `${window.location.origin}/api/books?order%5Bid%5D=ASC&page=2`,
+      );
+    });
+  });
+
   test('fetch a get_many resource', async () => {
     const mockHttpClient = jest.fn();
     mockHttpClient
diff --git a/src/parameterFactory.test.js b/src/parameterFactory.test.js
new file mode 100644
index 0000000..996f434
--- /dev/null
+++ b/src/parameterFactory.test.js
@@ -0,0 +1,116 @@
+import {
+  DateInput,
+  NullableBooleanInput,
+  NumberInput,
+  TextInput,
+} from 'react-admin';
+import parameterFactory from './parameterFactory';
+
+function mockParameter(range, variable = 'defaultValue') {
+  return {
+    range: 'http://www.w3.org/2001/XMLSchema#' + range,
+    variable,
+  };
+}
+
+describe('generate parameters input component', () => {
+  const options = {option1: 'some_option'};
+
+  function expectResultOfType(result, type) {
+    expect(result.type.prototype).toEqual(type.prototype);
+  }
+
+  function expectResultToHaveOptionsPropsAndParameterVariable(
+    result,
+    parameter,
+    options,
+  ) {
+    const props = result.props;
+    Object.keys(options).forEach(key => {
+      expect(props).toHaveProperty(key, options[key]);
+    });
+    expect(props).toHaveProperty('source', parameter.variable);
+  }
+
+  describe('generate parameter input component based on parameter range', () => {
+    const rangeToComponentType = {
+      string: TextInput,
+      integer: NumberInput,
+      float: NumberInput,
+      boolean: NullableBooleanInput,
+      dateTime: DateInput,
+    };
+
+    Object.keys(rangeToComponentType).forEach(function(range) {
+      describe(
+        "generate parameter input component for parameter with range '" +
+          range +
+          "'",
+        () => {
+          let parameter;
+          let result;
+
+          beforeEach(() => {
+            parameter = mockParameter(range);
+
+            result = parameterFactory(parameter, options);
+          });
+
+          test('generated input component is instance of', () => {
+            const expectedComponentType = rangeToComponentType[range];
+
+            expectResultOfType(result, expectedComponentType);
+          });
+
+          test('generated input component has options passed as props', () => {
+            expectResultToHaveOptionsPropsAndParameterVariable(
+              result,
+              parameter,
+              options,
+            );
+          });
+        },
+      );
+    });
+  });
+
+  test("TODO: generate parameter input component for parameter with variable 'between'", () => {
+    const result = parameterFactory(
+      mockParameter(null, 'someVariable[between]'),
+      options,
+    );
+
+    // Generation of input component is not implemented yet for parameter with variable 'between'
+    expect(result).toBeNull();
+  });
+
+  describe('generate parameter input component for option apiPlatform false', () => {
+    beforeEach(() => {
+      options.apiPlatform = false;
+    });
+
+    test("generate parameter input component for parameter with variable ending with square brackets if 'apiPlatform' is set to false", () => {
+      const parameter = mockParameter('string', 'someVariable[]');
+      const result = parameterFactory(parameter, options);
+
+      expectResultOfType(result, TextInput);
+      expectResultToHaveOptionsPropsAndParameterVariable(
+        result,
+        parameter,
+        options,
+      );
+    });
+
+    test("generate parameter input component for parameter with variable ending with 'order[xxx]' if 'apiPlatform' is set to false", () => {
+      const parameter = mockParameter('string', 'order[yyyy]');
+      const result = parameterFactory(parameter, options);
+
+      expectResultOfType(result, TextInput);
+      expectResultToHaveOptionsPropsAndParameterVariable(
+        result,
+        parameter,
+        options,
+      );
+    });
+  });
+});
diff --git a/src/resourceFactory.test.js b/src/resourceFactory.test.js
index 0559b51..9460fc4 100644
--- a/src/resourceFactory.test.js
+++ b/src/resourceFactory.test.js
@@ -12,6 +12,7 @@ describe('makes Resource component', () => {
 
   const fieldFactory = jest.fn();
   const inputFactory = jest.fn();
+  const parameterFactory = jest.fn();
 
   describe('makes default Resource component', () => {
     const resourceComponent = resourceFactory(
@@ -19,6 +20,7 @@ describe('makes Resource component', () => {
       api,
       fieldFactory,
       inputFactory,
+      parameterFactory,
     );
 
     test('is Resource component', () => {
@@ -51,6 +53,7 @@ describe('makes Resource component', () => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
         resource,
       });
     });
@@ -80,6 +83,7 @@ describe('makes Resource component', () => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
       );
 
       expect(resourceComponent.props.create).toEqual(customCreate);
@@ -94,6 +98,7 @@ describe('makes Resource component', () => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
       );
 
       expect(resourceComponent.props.list).toEqual(customList);
@@ -108,6 +113,7 @@ describe('makes Resource component', () => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
       );
 
       expect(resourceComponent.props.show).toEqual(customShow);
@@ -122,6 +128,7 @@ describe('makes Resource component', () => {
         api,
         fieldFactory,
         inputFactory,
+        parameterFactory,
       );
 
       expect(resourceComponent.props.edit).toEqual(customEdit);