Repository: api-platform/admin
Pull request #89 (2018-03-27 22:40:33)

Improve identifier management
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | 



Patch:
diff --git a/src/Edit.js b/src/Edit.js
index 5b14c0e..b8dad5c 100644
--- a/src/Edit.js
+++ b/src/Edit.js
@@ -4,6 +4,12 @@ import {DisabledInput, Edit as BaseEdit, SimpleForm} from 'admin-on-rest';
 import PropTypes from 'prop-types';
 import React from 'react';
 
+const hasIdentifier = fields => {
+  return (
+    undefined !== fields.find(({id}) => 'http://schema.org/identifier' === id)
+  );
+};
+
 const resolveProps = props => {
   const {options} = props;
   const {inputFactory: defaultInputFactory, resource} = options;
@@ -29,12 +35,13 @@ const resolveProps = props => {
 const Edit = props => {
   const {
     options: {api, fields, inputFactory, resource},
+    addIdInput = false === hasIdentifier(fields),
   } = resolveProps(props);
 
   return (
     <BaseEdit {...props}>
       <SimpleForm>
-        <DisabledInput source="id" />
+        {addIdInput && <DisabledInput source="id" />}
         {fields.map(field =>
           inputFactory(field, {
             api,
@@ -47,6 +54,7 @@ const Edit = props => {
 };
 
 Edit.propTypes = {
+  addIdInput: PropTypes.bool,
   options: PropTypes.shape({
     api: PropTypes.instanceOf(Api).isRequired,
     inputFactory: PropTypes.func.isRequired,
diff --git a/src/List.js b/src/List.js
index 3491dff..355cde1 100644
--- a/src/List.js
+++ b/src/List.js
@@ -10,6 +10,12 @@ import {
 import PropTypes from 'prop-types';
 import React from 'react';
 
+const hasIdentifier = fields => {
+  return (
+    undefined !== fields.find(({id}) => 'http://schema.org/identifier' === id)
+  );
+};
+
 const resolveProps = props => {
   const {options} = props;
   const {fieldFactory: defaultFieldFactory, resource} = options;
@@ -34,10 +40,10 @@ const resolveProps = props => {
 
 const List = props => {
   const {
-    addIdField,
     hasEdit,
     hasShow,
     options: {api, fieldFactory, fields, resource},
+    addIdField = false === hasIdentifier(fields),
   } = resolveProps(props);
 
   return (
@@ -58,7 +64,6 @@ const List = props => {
 };
 
 List.defaultProps = {
-  addIdField: true,
   perPage: 30, // Default value in API Platform
 };
 
diff --git a/src/Show.js b/src/Show.js
index f4761e1..3f4133e 100644
--- a/src/Show.js
+++ b/src/Show.js
@@ -4,6 +4,12 @@ import {Show as BaseShow, SimpleShowLayout, TextField} from 'admin-on-rest';
 import PropTypes from 'prop-types';
 import React from 'react';
 
+const hasIdentifier = fields => {
+  return (
+    undefined !== fields.find(({id}) => 'http://schema.org/identifier' === id)
+  );
+};
+
 const resolveProps = props => {
   const {options} = props;
   const {fieldFactory: defaultFieldFactory, resource} = options;
@@ -28,8 +34,8 @@ const resolveProps = props => {
 
 const Show = props => {
   const {
-    addIdField,
     options: {api, fieldFactory, fields, resource},
+    addIdField = false === hasIdentifier(fields),
   } = resolveProps(props);
 
   return (
@@ -47,10 +53,6 @@ const Show = props => {
   );
 };
 
-Show.defaultProps = {
-  addIdField: true,
-};
-
 Show.propTypes = {
   addIdField: PropTypes.bool,
   options: PropTypes.shape({
diff --git a/src/hydra/hydraClient.js b/src/hydra/hydraClient.js
index e60a0dd..b92b6b8 100644
--- a/src/hydra/hydraClient.js
+++ b/src/hydra/hydraClient.js
@@ -140,9 +140,9 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
   };
 
   /**
-   * @param {Object} resource 
-   * @param {Object} data 
-   * 
+   * @param {Object} resource
+   * @param {Object} data
+   *
    * @returns {Promise}
    */
   const transformAORDataToRequestBody = (resource, data = {}) => {
@@ -168,16 +168,15 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
   const convertAORRequestToHydraRequest = (type, resource, params) => {
     switch (type) {
       case CREATE:
-        return transformAORDataToRequestBody(
-          resource,
-          params.data,
-        ).then(body => ({
-          options: {
-            body,
-            method: 'POST',
-          },
-          url: `${entrypoint}/${resource}`,
-        }));
+        return transformAORDataToRequestBody(resource, params.data).then(
+          body => ({
+            options: {
+              body,
+              method: 'POST',
+            },
+            url: `${entrypoint}/${resource}`,
+          }),
+        );
 
       case DELETE:
         return Promise.resolve({
@@ -220,16 +219,15 @@ export default ({entrypoint, resources = []}, httpClient = fetchHydra) => {
         });
 
       case UPDATE:
-        return transformAORDataToRequestBody(
-          resource,
-          params.data,
-        ).then(body => ({
-          options: {
-            body,
-            method: 'PUT',
-          },
-          url: entrypoint + params.id,
-        }));
+        return transformAORDataToRequestBody(resource, params.data).then(
+          body => ({
+            options: {
+              body,
+              method: 'PUT',
+            },
+            url: entrypoint + params.id,
+          }),
+        );
 
       default:
         throw new Error(`Unsupported fetch action type ${type}`);
diff --git a/src/inputFactory.js b/src/inputFactory.js
index c57fc1c..368d502 100644
--- a/src/inputFactory.js
+++ b/src/inputFactory.js
@@ -14,6 +14,7 @@ import getReferenceNameField from './getReferenceNameField';
 
 export default (field, options) => {
   const props = {...field.inputProps};
+
   if (field.input) {
     return (
       <field.input
@@ -55,6 +56,21 @@ export default (field, options) => {
     );
   }
 
+  if ('http://schema.org/identifier' === field.id) {
+    const {
+      resource: {name},
+      prefix = `/${name}/`,
+    } = options;
+
+    props.format = value => {
+      return 0 === value.indexOf(prefix) ? value.substr(prefix.length) : value;
+    };
+
+    props.parse = value => {
+      return -1 !== value.indexOf(prefix) ? prefix + value : value;
+    };
+  }
+
   switch (field.range) {
     case 'http://www.w3.org/2001/XMLSchema#integer':
       return <NumberInput key={field.name} source={field.name} {...props} />;


Test patch:
diff --git a/src/Edit.test.js b/src/Edit.test.js
index da43ec4..7c09bae 100644
--- a/src/Edit.test.js
+++ b/src/Edit.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 'admin-on-rest';
+import {DisabledInput, TextInput} from 'admin-on-rest';
 import {shallow} from 'enzyme';
 import React from 'react';
 import Edit from './Edit';
@@ -65,6 +65,40 @@ describe('<Edit />', () => {
     );
 
     expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    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(TextInput).get(0).props.source).toEqual('fieldA');
+    expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
+  });
+
+  test('without default identifier input', () => {
+    const defaultInputFactory = jest.fn(inputFactory);
+
+    const resource = new Resource(resourceData.name, resourceData.url, {
+      ...resourceData,
+      editProps: {
+        addIdInput: false,
+      },
+    });
+
+    const api = new Api(apiData.entrypoint, {
+      ...apiData,
+      resources: [resource],
+    });
+
+    const render = shallow(
+      <Edit
+        options={{
+          api,
+          inputFactory: defaultInputFactory,
+          resource,
+        }}
+      />,
+    );
+
+    expect(defaultInputFactory).toHaveBeenCalledTimes(2);
+    expect(render.find(DisabledInput).length).toEqual(0);
     expect(render.find(TextInput).length).toEqual(2);
     expect(render.find(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
@@ -100,6 +134,8 @@ describe('<Edit />', () => {
 
     expect(customInputFactory).toHaveBeenCalledTimes(2);
     expect(defaultInputFactory).toHaveBeenCalledTimes(0);
+    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(TextInput).get(0).props.source).toEqual('fieldA');
     expect(render.find(TextInput).get(1).props.source).toEqual('fieldB');
@@ -136,7 +172,51 @@ describe('<Edit />', () => {
     );
 
     expect(defaultInputFactory).toHaveBeenCalledTimes(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(TextInput).get(0).props.source).toEqual('fieldC');
   });
+
+  test('with writable identifier', () => {
+    const defaultInputFactory = jest.fn(inputFactory);
+
+    const resource = new Resource(resourceData.name, resourceData.url, {
+      ...resourceData,
+      editProps: {
+        addIdInput: false,
+      },
+      writableFields: [
+        ...resourceData.writableFields,
+        new Field('id', {
+          id: 'http://schema.org/identifier',
+          range: 'http://www.w3.org/2001/XMLSchema#string',
+          reference: null,
+          required: true,
+        }),
+      ],
+    });
+
+    const api = new Api(apiData.entrypoint, {
+      ...apiData,
+      resources: [resource],
+    });
+
+    const render = shallow(
+      <Edit
+        options={{
+          api,
+          inputFactory: defaultInputFactory,
+          resource,
+        }}
+      />,
+    );
+
+    expect(defaultInputFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(DisabledInput).length).toEqual(0);
+    expect(render.find(TextInput).length).toEqual(3);
+    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');
+  });
 });
diff --git a/src/List.test.js b/src/List.test.js
index 9b000c3..d8da919 100644
--- a/src/List.test.js
+++ b/src/List.test.js
@@ -73,7 +73,7 @@ describe('<List />', () => {
     expect(render.find(TextField).get(2).props.source).toEqual('fieldB');
   });
 
-  test('without adding ID', () => {
+  test('without default identifier field', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
@@ -181,4 +181,47 @@ describe('<List />', () => {
     expect(render.find(TextField).get(0).props.source).toEqual('id');
     expect(render.find(TextField).get(1).props.source).toEqual('fieldC');
   });
+
+  test('with readable identifier', () => {
+    const defaultFieldFactory = jest.fn(fieldFactory);
+
+    const resource = new Resource(resourceData.name, resourceData.url, {
+      ...resourceData,
+      listProps: {
+        addIdField: false,
+      },
+      readableFields: [
+        ...resourceData.readableFields,
+        new Field('id', {
+          id: 'http://schema.org/identifier',
+          range: 'http://www.w3.org/2001/XMLSchema#string',
+          reference: null,
+          required: true,
+        }),
+      ],
+    });
+
+    const api = new Api(apiData.entrypoint, {
+      ...apiData,
+      resources: [resource],
+    });
+
+    const render = shallow(
+      <List
+        hasEdit={true}
+        hasShow={true}
+        options={{
+          api,
+          fieldFactory: defaultFieldFactory,
+          resource,
+        }}
+      />,
+    );
+
+    expect(defaultFieldFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(TextField).length).toEqual(3);
+    expect(render.find(TextField).get(0).props.source).toEqual('fieldA');
+    expect(render.find(TextField).get(1).props.source).toEqual('fieldB');
+    expect(render.find(TextField).get(2).props.source).toEqual('id');
+  });
 });
diff --git a/src/Show.test.js b/src/Show.test.js
index 51d96c8..cd29adc 100644
--- a/src/Show.test.js
+++ b/src/Show.test.js
@@ -71,7 +71,7 @@ describe('<Show />', () => {
     expect(render.find(TextField).get(2).props.source).toEqual('fieldB');
   });
 
-  test('without adding ID', () => {
+  test('without default identifier field', () => {
     const defaultFieldFactory = jest.fn(fieldFactory);
 
     const resource = new Resource(resourceData.name, resourceData.url, {
@@ -173,4 +173,45 @@ describe('<Show />', () => {
     expect(render.find(TextField).get(0).props.source).toEqual('id');
     expect(render.find(TextField).get(1).props.source).toEqual('fieldC');
   });
+
+  test('with readable identifier', () => {
+    const defaultFieldFactory = jest.fn(fieldFactory);
+
+    const resource = new Resource(resourceData.name, resourceData.url, {
+      ...resourceData,
+      readableFields: [
+        ...resourceData.readableFields,
+        new Field('id', {
+          id: 'http://schema.org/identifier',
+          range: 'http://www.w3.org/2001/XMLSchema#string',
+          reference: null,
+          required: true,
+        }),
+      ],
+      showProps: {
+        addIdField: false,
+      },
+    });
+
+    const api = new Api(apiData.entrypoint, {
+      ...apiData,
+      resources: [resource],
+    });
+
+    const render = shallow(
+      <Show
+        options={{
+          api,
+          fieldFactory: defaultFieldFactory,
+          resource,
+        }}
+      />,
+    );
+
+    expect(defaultFieldFactory).toHaveBeenCalledTimes(3);
+    expect(render.find(TextField).length).toEqual(3);
+    expect(render.find(TextField).get(0).props.source).toEqual('fieldA');
+    expect(render.find(TextField).get(1).props.source).toEqual('fieldB');
+    expect(render.find(TextField).get(2).props.source).toEqual('id');
+  });
 });