Repository: api-platform/admin
Pull request #302 (2020-05-30 14:09:59)

Embedded support for array relation
| Q             | A
| ------------- | ---
| Bug fix?      | yes and no
| New feature?  | yes and no
| BC breaks?    | slightly
| Deprecations? | no
| Tickets       | N/A
| License       | MIT
| Doc PR        | N/A

Following https://github.com/api-platform/admin/pull/299.

The parameter `useEmbedded` now works with an array of embeddeds too.

If not enabled and if an array of embeddeds is used, the IRIs will be displayed as text (no reference anymore).


Patch:
diff --git a/src/FieldGuesser.js b/src/FieldGuesser.js
index d90c55e..5d37127 100644
--- a/src/FieldGuesser.js
+++ b/src/FieldGuesser.js
@@ -1,6 +1,7 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import {
+  ArrayField,
   BooleanField,
   ChipField,
   DateField,
@@ -8,6 +9,7 @@ import {
   NumberField,
   ReferenceField,
   ReferenceArrayField,
+  SimpleList,
   SingleFieldList,
   TextField,
   UrlField,
@@ -24,18 +26,6 @@ const isFieldSortable = (field, schema) => {
   );
 };
 
-const renderReferenceArrayField = (referenceField, schemaAnalyzer, props) => {
-  const fieldName = schemaAnalyzer.getFieldNameFromSchema(referenceField);
-
-  return (
-    <ReferenceArrayField reference={referenceField.name} {...props}>
-      <SingleFieldList>
-        <ChipField source={fieldName} key={fieldName} />
-      </SingleFieldList>
-    </ReferenceArrayField>
-  );
-};
-
 const renderField = (field, schemaAnalyzer, props) => {
   if (null !== field.reference) {
     if (1 === field.maxCardinality) {
@@ -48,11 +38,27 @@ const renderField = (field, schemaAnalyzer, props) => {
       );
     }
 
-    return renderReferenceArrayField(field.reference, schemaAnalyzer, props);
+    const fieldName = schemaAnalyzer.getFieldNameFromSchema(field.reference);
+    return (
+      <ReferenceArrayField reference={field.reference.name} {...props}>
+        <SingleFieldList>
+          <ChipField source={fieldName} key={fieldName} />
+        </SingleFieldList>
+      </ReferenceArrayField>
+    );
   }
 
   if (null !== field.embedded && 1 !== field.maxCardinality) {
-    return renderReferenceArrayField(field.embedded, schemaAnalyzer, props);
+    return (
+      <ArrayField {...props}>
+        <SimpleList
+          primaryText={(record) => JSON.stringify(record)}
+          linkType={false}
+          // Workaround for forcing the list to display underlying data.
+          total={2}
+        />
+      </ArrayField>
+    );
   }
 
   const fieldType = schemaAnalyzer.getFieldType(field);
diff --git a/src/InputGuesser.js b/src/InputGuesser.js
index 7f087fb..744319f 100644
--- a/src/InputGuesser.js
+++ b/src/InputGuesser.js
@@ -16,27 +16,6 @@ import {
 } from 'react-admin';
 import Introspecter from './Introspecter';
 
-const renderReferenceArrayInput = (
-  field,
-  referenceField,
-  schemaAnalyzer,
-  validate,
-  props,
-) => (
-  <ReferenceArrayInput
-    key={field.name}
-    label={field.name}
-    reference={referenceField.name}
-    source={field.name}
-    validate={validate}
-    {...props}
-    allowEmpty>
-    <SelectArrayInput
-      optionText={schemaAnalyzer.getFieldNameFromSchema(referenceField)}
-    />
-  </ReferenceArrayInput>
-);
-
 export const IntrospectedInputGuesser = ({
   fields,
   readableFields,
@@ -75,22 +54,19 @@ export const IntrospectedInputGuesser = ({
       );
     }
 
-    return renderReferenceArrayInput(
-      field,
-      field.reference,
-      schemaAnalyzer,
-      validate,
-      props,
-    );
-  }
-
-  if (null !== field.embedded && 1 !== field.maxCardinality) {
-    return renderReferenceArrayInput(
-      field,
-      field.embedded,
-      schemaAnalyzer,
-      validate,
-      props,
+    return (
+      <ReferenceArrayInput
+        key={field.name}
+        label={field.name}
+        reference={field.reference.name}
+        source={field.name}
+        validate={validate}
+        {...props}
+        allowEmpty>
+        <SelectArrayInput
+          optionText={schemaAnalyzer.getFieldNameFromSchema(field.reference)}
+        />
+      </ReferenceArrayInput>
     );
   }
 
@@ -110,12 +86,23 @@ export const IntrospectedInputGuesser = ({
     };
   }
 
+  const formatEmbedded = (value) => JSON.stringify(value);
+  const parseEmbedded = (value) => JSON.parse(value);
+
   if (null !== field.embedded && 1 === field.maxCardinality) {
-    props.format = (value) => JSON.stringify(value);
+    props.format = formatEmbedded;
+    props.parse = parseEmbedded;
   }
 
   switch (fieldType) {
     case 'array':
+      let textInputFormat = (value) => value;
+      let textInputParse = (value) => value;
+      if (null !== field.embedded && 1 !== field.maxCardinality) {
+        textInputFormat = formatEmbedded;
+        textInputParse = parseEmbedded;
+      }
+
       return (
         <ArrayInput
           key={field.name}
@@ -123,7 +110,7 @@ export const IntrospectedInputGuesser = ({
           validate={validate}
           {...props}>
           <SimpleFormIterator>
-            <TextInput />
+            <TextInput format={textInputFormat} parse={textInputParse} />
           </SimpleFormIterator>
         </ArrayInput>
       );
diff --git a/src/hydra/dataProvider.js b/src/hydra/dataProvider.js
index 269745b..334a5c0 100644
--- a/src/hydra/dataProvider.js
+++ b/src/hydra/dataProvider.js
@@ -93,7 +93,7 @@ export const transformJsonLdDocumentToReactAdminDocument = (
           ] = transformJsonLdDocumentToReactAdminDocument(obj, false, false);
         }
 
-        return obj['@id'];
+        return useEmbedded ? obj : obj['@id'];
       });
     }
   });
diff --git a/src/hydra/schemaAnalyzer.js b/src/hydra/schemaAnalyzer.js
index ba1b92b..d7c33f0 100644
--- a/src/hydra/schemaAnalyzer.js
+++ b/src/hydra/schemaAnalyzer.js
@@ -75,6 +75,10 @@ const getFieldType = (field) => {
     default:
   }
 
+  if (null !== field.embedded && 1 !== field.maxCardinality) {
+    return 'array';
+  }
+
   switch (field.range) {
     case 'http://www.w3.org/2001/XMLSchema#array':
       return 'array';


Test patch:
diff --git a/src/hydra/dataProvider.test.js b/src/hydra/dataProvider.test.js
index b09d7a6..93bac62 100644
--- a/src/hydra/dataProvider.test.js
+++ b/src/hydra/dataProvider.test.js
@@ -13,6 +13,13 @@ const EMBEDDED_ITEM = {
   dateCreated: '2017-04-25T00:00:00+00:00',
 };
 
+const EMBEDDED_COMMENT = {
+  '@id': '/comments/1',
+  '@type': 'http://schema.org/Comment',
+  text: 'Lorem ipsum dolor sit amet.',
+  dateCreated: '2017-04-26T00:00:00+00:00',
+};
+
 const JSON_LD_DOCUMENT = {
   '@id': '/reviews/327',
   id: 327,
@@ -22,12 +29,7 @@ const JSON_LD_DOCUMENT = {
   rating: 3,
   itemReviewed: EMBEDDED_ITEM,
   comment: [
-    {
-      '@id': '/comments/1',
-      '@type': 'http://schema.org/Comment',
-      text: 'Lorem ipsum dolor sit amet.',
-      dateCreated: '2017-04-26T00:00:00+00:00',
-    },
+    EMBEDDED_COMMENT,
     {
       '@id': '/comments/2',
       '@type': 'http://schema.org/Comment',
@@ -74,8 +76,10 @@ describe('Transform a JSON-LD document to a React Admin compatible document', ()
     );
   });
 
-  test('transform arrays of embedded documents to their IRIs', () => {
-    expect(reactAdminDocument.comment[0]).toBe('/comments/1');
+  test('keep arrays of embedded documents', () => {
+    expect(JSON.stringify(reactAdminDocument.comment[0])).toBe(
+      JSON.stringify(EMBEDDED_COMMENT),
+    );
   });
 });