Repository: api-platform/admin
Pull request #527 (2024-02-15 17:00:20)

Show|Create|EditGuesser viewComponent prop
In Create/EditGuesser form component can be customised by passing down `formComponent` prop as renamed `component` prop (to avoid name collision). Guessers use base RA components that also can take `component` prop for such customisation but it is omitted, so any further customisation needs a lot of code to assembly custom guesser replacement, so for completeness  and simplifying any customisation we can pass this other renamed component prop in this way. WDYT? 

See: 
- https://marmelab.com/react-admin/Show.html#component
- https://marmelab.com/react-admin/Create.html#component
- https://marmelab.com/react-admin/Edit.html#component


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/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',
+    );
+  });
 });