Repository: api-platform/admin
Pull request #83 (2018-02-24 18:12:11)

Manage "order" in hydraClient
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR gives the possibility to order entries in `List` view.

I use [qs](https://github.com/ljharb/qs) instead of `admin-on-rest::fetchUtils::queryParameters` (alias for [query-string](https://github.com/sindresorhus/query-string)) because it does not manage nested level (see https://github.com/sindresorhus/query-string/issues/100 and https://github.com/sindresorhus/query-string#nesting)

mauchede: @dunglas: I will add a test.

Patch:
diff --git a/package.json b/package.json
index 2b6bd0e..2b3b4d9 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,8 @@
     "babel-runtime": "^6.23",
     "jsonld": "^0.4",
     "lodash.isplainobject": "^4.0.6",
-    "prop-types": "^15.5.7"
+    "prop-types": "^15.5.7",
+    "qs": "^6.5.1"
   },
   "peerDependencies": {
     "react": "^15.4.0 || ^16.0.0",
diff --git a/src/AdminBuilder.js b/src/AdminBuilder.js
index 16e5036..abffab8 100644
--- a/src/AdminBuilder.js
+++ b/src/AdminBuilder.js
@@ -1,19 +1,48 @@
 import Api from '@api-platform/api-doc-parser/lib/Api';
-import {Admin} from 'admin-on-rest';
+import {Admin, Delete, Resource} from 'admin-on-rest';
 import PropTypes from 'prop-types';
 import React from 'react';
+import Create from './Create';
+import Edit from './Edit';
 import fieldFactory from './fieldFactory';
 import inputFactory from './inputFactory';
-import resourceFactory from './resourceFactory';
+import List from './List';
+import Show from './Show';
 
 const AdminBuilder = props => {
   const {api, fieldFactory, inputFactory, title = api.title} = props;
 
   return (
     <Admin {...props} title={title}>
-      {api.resources.map(resource =>
-        resourceFactory(resource, api, fieldFactory, inputFactory),
-      )}
+      {api.resources.map(resource => {
+        const {
+          create = Create,
+          edit = Edit,
+          list = List,
+          name,
+          props,
+          remove = Delete,
+          show = Show,
+        } = resource;
+        return (
+          <Resource
+            {...props}
+            create={create}
+            edit={edit}
+            key={name}
+            list={list}
+            name={name}
+            options={{
+              api,
+              fieldFactory,
+              inputFactory,
+              resource,
+            }}
+            remove={remove}
+            show={show}
+          />
+        );
+      })}
     </Admin>
   );
 };
diff --git a/src/hydra/hydraClient.js b/src/hydra/hydraClient.js
index d850184..d3e070d 100644
--- a/src/hydra/hydraClient.js
+++ b/src/hydra/hydraClient.js
@@ -1,7 +1,6 @@
 import {
   CREATE,
   DELETE,
-  fetchUtils,
   GET_LIST,
   GET_MANY,
   GET_MANY_REFERENCE,
@@ -9,6 +8,7 @@ import {
   UPDATE,
 } from 'admin-on-rest';
 import isPlainObject from 'lodash.isplainobject';
+import qs from 'qs';
 import fetchHydra from './fetchHydra';
 
 /**
@@ -125,12 +125,15 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
         });
 
       case GET_LIST: {
-        const {pagination: {page}} = params;
+        const {pagination: {page}, sort: {field, order}} = params;
 
         return Promise.resolve({
           options: {},
-          url: `${entrypoint}/${resource}?${fetchUtils.queryParameters({
+          url: `${entrypoint}/${resource}?${qs.stringify({
             ...params.filter,
+            order: {
+              [field]: order,
+            },
             page,
           })}`,
         });
@@ -139,7 +142,7 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
       case GET_MANY_REFERENCE:
         return Promise.resolve({
           options: {},
-          url: `${entrypoint}/${resource}?${fetchUtils.queryParameters({
+          url: `${entrypoint}/${resource}?${qs.stringify({
             [params.target]: params.id,
           })}`,
         });
diff --git a/src/index.js b/src/index.js
index dc611c2..5b59ba5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,7 +3,6 @@ export Create from './Create';
 export Edit from './Edit';
 export fieldFactory from './fieldFactory';
 export inputFactory from './inputFactory';
-export resourceFactory from './resourceFactory';
 export List from './List';
 export Show from './Show';
 export * from './hydra';
diff --git a/src/resourceFactory.js b/src/resourceFactory.js
deleted file mode 100644
index b8ea427..0000000
--- a/src/resourceFactory.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import {Delete, Resource} from 'admin-on-rest';
-import React from 'react';
-import Create from './Create';
-import Edit from './Edit';
-import List from './List';
-import Show from './Show';
-
-export default (resource, api, fieldFactory, inputFactory) => {
-  const {
-    create = Create,
-    edit = Edit,
-    list = List,
-    name,
-    props,
-    remove = Delete,
-    show = Show,
-  } = resource;
-  return (
-    <Resource
-      {...props}
-      create={create}
-      edit={edit}
-      key={name}
-      list={list}
-      name={name}
-      options={{
-        api,
-        fieldFactory,
-        inputFactory,
-        resource,
-      }}
-      remove={remove}
-      show={show}
-    />
-  );
-};
diff --git a/yarn.lock b/yarn.lock
index e6a116a..44659a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3295,14 +3295,14 @@ punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
 
+qs@^6.5.1, qs@~6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
 qs@~6.4.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
 
-qs@~6.5.1:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
-
 query-string@~5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88"


Test patch:
diff --git a/src/hydra/hydraClient.test.js b/src/hydra/hydraClient.test.js
index 23013ff..ed498b7 100644
--- a/src/hydra/hydraClient.test.js
+++ b/src/hydra/hydraClient.test.js
@@ -1,5 +1,5 @@
+import {GET_LIST, GET_MANY} from 'admin-on-rest';
 import hydraClient, {transformJsonLdDocumentToAORDocument} from './hydraClient';
-import {GET_MANY} from 'admin-on-rest';
 
 describe('map a json-ld document to an admin on rest compatible document', () => {
   const jsonLdDocument = {
@@ -76,6 +76,26 @@ describe('map a json-ld document to an admin on rest compatible document', () =>
 });
 
 describe('fetch data from an hydra api', () => {
+  test('fetch a get_list resource', async () => {
+    const mockHttpClient = jest.fn();
+    mockHttpClient.mockReturnValue(
+      Promise.resolve({
+        json: {
+          'hydra:member': [{'@id': 'books/5'}],
+        },
+      }),
+    );
+
+    await hydraClient({entrypoint: ''}, mockHttpClient)(GET_LIST, 'books', {
+      pagination: {page: 2},
+      sort: {field: 'id', order: 'ASC'},
+    }).then(() => {
+      expect(mockHttpClient.mock.calls[0][0]).toBe(
+        '/books?order%5Bid%5D=ASC&page=2',
+      );
+    });
+  });
+
   test('fetch a get_many resource', async () => {
     const mockHttpClient = jest.fn();
     mockHttpClient
diff --git a/src/resourceFactory.test.js b/src/resourceFactory.test.js
deleted file mode 100644
index 68637c5..0000000
--- a/src/resourceFactory.test.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import {Resource} from 'admin-on-rest';
-import resourceFactory from './resourceFactory';
-
-describe('makes Resource component', () => {
-  const resource = {
-    name: 'name',
-    props: {custom: 'props'},
-  };
-  const api = {
-    name: 'api',
-  };
-
-  const fieldFactory = jest.fn();
-  const inputFactory = jest.fn();
-
-  describe('makes default Resource component', () => {
-    const resourceComponent = resourceFactory(
-      resource,
-      api,
-      fieldFactory,
-      inputFactory,
-    );
-
-    test('is Resource component', () => {
-      expect(resourceComponent.type.prototype).toEqual(Resource.prototype);
-    });
-
-    test('have props defined', () => {
-      expect(resourceComponent.props).toBeDefined();
-    });
-
-    test('have proper name prop set', () => {
-      expect(resourceComponent.props.name).toEqual(resource.name);
-    });
-
-    test('have default actions: create, edit, list, remove, show set', () => {
-      expect(resourceComponent.props.create).toBeInstanceOf(Function);
-      expect(resourceComponent.props.edit).toBeInstanceOf(Function);
-      expect(resourceComponent.props.list).toBeInstanceOf(Function);
-      expect(resourceComponent.props.remove).toBeInstanceOf(Function);
-      expect(resourceComponent.props.show).toBeInstanceOf(Function);
-    });
-
-    test('have proper custom props set', () => {
-      Object.keys(resource.props).forEach(key => {
-        expect(resourceComponent.props[key]).toEqual(resource.props[key]);
-      });
-    });
-
-    test('have proper options prop set', () => {
-      expect(resourceComponent.props.options).toEqual({
-        api,
-        fieldFactory,
-        inputFactory,
-        resource,
-      });
-    });
-  });
-
-  describe('makes Resource component with custom actions', () => {
-    test('have custom create action', () => {
-      const customCreate = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          create: customCreate,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.create).toEqual(customCreate);
-    });
-
-    test('have custom create action', () => {
-      const customCreate = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          create: customCreate,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.create).toEqual(customCreate);
-    });
-
-    test('have custom list action', () => {
-      const customList = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          list: customList,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.list).toEqual(customList);
-    });
-
-    test('have custom show action', () => {
-      const customShow = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          show: customShow,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.show).toEqual(customShow);
-    });
-
-    test('have custom remove action', () => {
-      const customDelete = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          remove: customDelete,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.remove).toEqual(customDelete);
-    });
-
-    test('have custom edit action', () => {
-      const customEdit = jest.fn();
-      const resourceComponent = resourceFactory(
-        Object.assign({}, resource, {
-          edit: customEdit,
-        }),
-        api,
-        fieldFactory,
-        inputFactory,
-      );
-
-      expect(resourceComponent.props.edit).toEqual(customEdit);
-    });
-  });
-});