Repository: api-platform/admin
Pull request #528 (2024-02-19 17:21:23)

null violations error body fix
I seems that PR #515 broke violations list processing based on error body so validation messages are not shown any more in the form (?), this would never happen:

https://github.com/api-platform/admin/blob/a3292d81ddab39c04152e239e641920dd6f156ff/src/hydra/fetchHydra.ts#L79-L81

I think that we should step back and let jsonld do the math like it was in previous version. With this fix validation errors are shown again in the form. Please correct me if I missed anything here.

dominikboniecki: @alanpoulain please look at this change, the bug is very critical for global error handling in a whole project

Patch:
diff --git a/src/CreateGuesser.tsx b/src/CreateGuesser.tsx
index 77d8754..4faacde 100644
--- a/src/CreateGuesser.tsx
+++ b/src/CreateGuesser.tsx
@@ -59,6 +59,7 @@ export const IntrospectedCreateGuesser = ({
   warnWhenUnsavedChanges,
   sanitizeEmptyValues = true,
   formComponent,
+  viewComponent,
   children,
   ...props
 }: IntrospectedCreateGuesserProps) => {
@@ -162,7 +163,7 @@ export const IntrospectedCreateGuesser = ({
   const FormType = hasFormTab ? TabbedForm : SimpleForm;
 
   return (
-    <Create resource={resource} {...props}>
+    <Create resource={resource} component={viewComponent} {...props}>
       <FormType
         onSubmit={save}
         mode={mode}
diff --git a/src/EditGuesser.tsx b/src/EditGuesser.tsx
index 9876f97..7c21cb4 100644
--- a/src/EditGuesser.tsx
+++ b/src/EditGuesser.tsx
@@ -62,6 +62,7 @@ export const IntrospectedEditGuesser = ({
   toolbar,
   warnWhenUnsavedChanges,
   formComponent,
+  viewComponent,
   sanitizeEmptyValues = true,
   children,
   ...props
@@ -197,6 +198,7 @@ export const IntrospectedEditGuesser = ({
       id={id}
       mutationMode={mutationMode}
       redirect={redirectTo}
+      component={viewComponent}
       transform={(data: Partial<RaRecord>) => ({
         ...data,
         extraInformation: { hasFileField },
diff --git a/src/FieldGuesser.tsx b/src/FieldGuesser.tsx
index 90e123d..d715d81 100644
--- a/src/FieldGuesser.tsx
+++ b/src/FieldGuesser.tsx
@@ -23,6 +23,7 @@ import type {
   NumberFieldProps,
   ReferenceArrayFieldProps,
   ReferenceFieldProps,
+  SingleFieldListProps,
   TextFieldProps,
   UrlFieldProps,
 } from 'react-admin';
@@ -65,11 +66,13 @@ const renderField = (
     }
 
     const fieldName = schemaAnalyzer.getFieldNameFromSchema(field.reference);
+    const { linkType, ...rest } = props as ReferenceArrayFieldProps &
+      Pick<SingleFieldListProps, 'linkType'>;
     return (
       <ReferenceArrayField
-        {...(props as ReferenceArrayFieldProps)}
+        {...(rest as ReferenceArrayFieldProps)}
         reference={field.reference.name}>
-        <SingleFieldList>
+        <SingleFieldList linkType={linkType}>
           <ChipField source={fieldName} key={fieldName} />
         </SingleFieldList>
       </ReferenceArrayField>
diff --git a/src/ListGuesser.tsx b/src/ListGuesser.tsx
index 6583f90..9d24464 100644
--- a/src/ListGuesser.tsx
+++ b/src/ListGuesser.tsx
@@ -70,6 +70,7 @@ export const IntrospectedListGuesser = ({
   empty,
   hover,
   expand,
+  expandSingle,
   optimized,
   size,
   children,
@@ -122,6 +123,7 @@ export const IntrospectedListGuesser = ({
         empty={empty}
         hover={hover}
         expand={expand}
+        expandSingle={expandSingle}
         optimized={optimized}
         size={size}
         sx={datagridSx}>
diff --git a/src/ShowGuesser.tsx b/src/ShowGuesser.tsx
index a71cfcb..bcb9959 100644
--- a/src/ShowGuesser.tsx
+++ b/src/ShowGuesser.tsx
@@ -44,6 +44,7 @@ export const IntrospectedShowGuesser = ({
   writableFields,
   schema,
   schemaAnalyzer,
+  viewComponent,
   children,
   ...props
 }: IntrospectedShowGuesserProps) => {
@@ -70,7 +71,7 @@ export const IntrospectedShowGuesser = ({
   const ShowLayout = hasTab ? TabbedShowLayout : SimpleShowLayout;
 
   return (
-    <Show {...props}>
+    <Show component={viewComponent} {...props}>
       <ShowLayout>{fieldChildren}</ShowLayout>
     </Show>
   );
diff --git a/src/hydra/dataProvider.ts b/src/hydra/dataProvider.ts
index bf1ff01..385ada4 100644
--- a/src/hydra/dataProvider.ts
+++ b/src/hydra/dataProvider.ts
@@ -388,7 +388,9 @@ function dataProvider(
         } = params as GetListParams | GetManyReferenceParams;
 
         if (order && field) {
-          url.searchParams.set(`order[${field}]`, order);
+          field.split(',').forEach((fieldName) => {
+            url.searchParams.set(`order[${fieldName.trim()}]`, order);
+          });
         }
         if (page) url.searchParams.set('page', page.toString());
         if (perPage) url.searchParams.set('itemsPerPage', perPage.toString());
diff --git a/src/hydra/fetchHydra.ts b/src/hydra/fetchHydra.ts
index dcc9f70..e64597c 100644
--- a/src/hydra/fetchHydra.ts
+++ b/src/hydra/fetchHydra.ts
@@ -4,7 +4,7 @@ import {
   getDocumentationUrlFromHeaders,
 } from '@api-platform/api-doc-parser';
 import jsonld from 'jsonld';
-import type { ContextDefinition, NodeObject } from 'jsonld';
+import type { NodeObject } from 'jsonld';
 import type { JsonLdObj } from 'jsonld/jsonld-spec';
 import type { HttpClientOptions, HydraHttpClientResponse } from '../types.js';
 
@@ -55,13 +55,11 @@ function fetchHydra(
         });
       };
 
-      return documentLoader(getDocumentationUrlFromHeaders(headers))
-        .then((response) =>
-          jsonld.expand(body, {
-            expandContext: response.document as ContextDefinition,
-          }),
-        )
-
+      return jsonld
+        .expand(body, {
+          base: getDocumentationUrlFromHeaders(headers),
+          documentLoader,
+        })
         .then((json) =>
           Promise.reject(
             new HttpError(
diff --git a/src/types.ts b/src/types.ts
index 30032f7..3a619a9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -47,6 +47,7 @@ import type {
   ResourceProps,
   ShowProps,
   SimpleFormProps,
+  SingleFieldListProps,
   TabbedFormProps,
   TextFieldProps,
   TextInputProps,
@@ -370,6 +371,7 @@ type CreateFormProps = Omit<
   CreateProps & SimpleFormProps & TabbedFormProps,
   'children'
 > &
+  Partial<PickRename<CreateProps, 'component', 'viewComponent'>> &
   Partial<
     PickRename<SimpleFormProps & TabbedFormProps, 'component', 'formComponent'>
   > & {
@@ -388,6 +390,7 @@ type EditFormProps = Omit<
   EditProps & SimpleFormProps & TabbedFormProps,
   'children'
 > &
+  Partial<PickRename<EditProps, 'component', 'viewComponent'>> &
   Partial<
     PickRename<SimpleFormProps & TabbedFormProps, 'component', 'formComponent'>
   > & {
@@ -421,9 +424,10 @@ export type ListGuesserProps = Omit<
 type ShowFormProps = Omit<
   ShowProps & SimpleFormProps & TabbedFormProps,
   'children'
-> & {
-  children?: ReactNode;
-};
+> &
+  Partial<PickRename<ShowProps, 'component', 'viewComponent'>> & {
+    children?: ReactNode;
+  };
 
 export type IntrospectedShowGuesserProps = ShowFormProps &
   IntrospectedGuesserProps;
@@ -452,7 +456,7 @@ export type FieldProps =
   | UrlFieldProps
   | EmailFieldProps
   | ArrayFieldProps
-  | ReferenceArrayFieldProps
+  | (ReferenceArrayFieldProps & Pick<SingleFieldListProps, 'linkType'>)
   | EnumFieldProps
   | ReferenceFieldProps;
 


Test patch:
diff --git a/src/hydra/dataProvider.test.ts b/src/hydra/dataProvider.test.ts
index 1d07e17..874ab8a 100644
--- a/src/hydra/dataProvider.test.ts
+++ b/src/hydra/dataProvider.test.ts
@@ -603,4 +603,45 @@ describe('Transform a React Admin request to an Hydra request', () => {
       'http://localhost/entrypoint/comments?order%5Bid%5D=ASC&page=1&itemsPerPage=30',
     );
   });
+
+  test('React Admin get list with compound order', async () => {
+    mockFetchHydra.mockClear();
+    mockFetchHydra.mockReturnValueOnce(
+      Promise.resolve({
+        status: 200,
+        headers: new Headers(),
+        json: {
+          'hydra:member': [
+            { '@id': '/comments/423' },
+            { '@id': '/comments/976' },
+          ],
+          'hydra:totalItems': 2,
+          'hydra:view': {
+            '@id': '/comments?page=1',
+            '@type': 'hydra:PartialCollectionView',
+            'hydra:first': '/comments?page=1',
+            'hydra:last': '/comments?page=1',
+            'hydra:next': '/comments?page=1',
+          },
+        },
+      }),
+    );
+    const result = await dataProvider.current.getList('comments', {
+      pagination: { page: 1, perPage: 30 },
+      sort: { field: 'text, id', order: 'DESC' },
+      filter: false,
+    });
+    expect(result).toEqual({
+      data: [
+        { '@id': '/comments/423', id: '/comments/423' },
+        { '@id': '/comments/976', id: '/comments/976' },
+      ],
+      total: 2,
+    });
+    const url = mockFetchHydra.mock.calls?.[0]?.[0] ?? new URL('https://foo');
+    expect(url).toBeInstanceOf(URL);
+    expect(url.toString()).toEqual(
+      'http://localhost/entrypoint/comments?order%5Btext%5D=DESC&order%5Bid%5D=DESC&page=1&itemsPerPage=30',
+    );
+  });
 });