From afd756d44aec01ef21709be3f1da4235e33cd024 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:45:00 +0100 Subject: [PATCH 01/28] Introduce SourcePrefixContext --- .../ra-core/src/core/SourcePrefixContext.tsx | 19 +++++ packages/ra-core/src/core/index.ts | 1 + .../src/form/FormDataConsumer.spec.tsx | 30 +------ .../ra-core/src/form/FormDataConsumer.tsx | 15 +++- .../src/form/useApplyInputDefaultValues.ts | 14 ++-- packages/ra-core/src/form/useInput.ts | 13 +-- packages/ra-core/src/util/FieldTitle.tsx | 5 +- .../src/input/ArrayInput/ArrayInput.spec.tsx | 80 ++++++++++--------- .../src/input/ArrayInput/ArrayInput.tsx | 21 ++--- .../ArrayInput/SimpleFormIterator.spec.tsx | 36 +-------- .../ArrayInput/SimpleFormIteratorItem.tsx | 26 +----- 11 files changed, 114 insertions(+), 146 deletions(-) create mode 100644 packages/ra-core/src/core/SourcePrefixContext.tsx diff --git a/packages/ra-core/src/core/SourcePrefixContext.tsx b/packages/ra-core/src/core/SourcePrefixContext.tsx new file mode 100644 index 00000000000..1d4465ea431 --- /dev/null +++ b/packages/ra-core/src/core/SourcePrefixContext.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; + +export type SourcePrefixContextValue = string; + +/** + * Context to that provides a possible prefix for the source prop of fields and inputs. + */ +export const SourcePrefixContext = createContext(''); + +export const SourcePrefixContextProvider = SourcePrefixContext.Provider; + +/** + * Hook to get the source prefix that a field or input should add to its source prop. + * @returns The source prefix that a field or input should add to its source prop. + */ +export const useSourcePrefix = (): string => { + const prefix = useContext(SourcePrefixContext); + return prefix ?? ''; +}; diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts index ac7a29add76..b8bfc93593d 100644 --- a/packages/ra-core/src/core/index.ts +++ b/packages/ra-core/src/core/index.ts @@ -13,3 +13,4 @@ export * from './useResourceContext'; export * from './useResourceDefinition'; export * from './useResourceDefinitions'; export * from './useGetRecordRepresentation'; +export * from './SourcePrefixContext'; diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 5f3fccd8e0d..faf56671a29 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -34,31 +34,6 @@ describe('FormDataConsumerView', () => { }); }); - it('calls its children function with scopedFormData and getSource if it received an index prop', () => { - const children = jest.fn(({ getSource }) => { - getSource('id'); - return null; - }); - const formData = { id: 123, title: 'A title', authors: [{ id: 0 }] }; - - render( - - {children} - - ); - - expect(children.mock.calls[0][0].formData).toEqual(formData); - expect(children.mock.calls[0][0].scopedFormData).toEqual({ id: 0 }); - expect(children.mock.calls[0][0].getSource('id')).toEqual( - 'authors[0].id' - ); - }); - it('calls its children with updated formData on first render', async () => { let globalFormData; render( @@ -130,10 +105,7 @@ describe('FormDataConsumerView', () => { globalScopedFormData = scopedFormData; return scopedFormData && scopedFormData.name ? ( - + ) : null; }} diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index aa79132d3f7..b3e6ea6fbf5 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; import { useFormContext, FieldValues } from 'react-hook-form'; import get from 'lodash/get'; import { useFormValues } from './useFormValues'; +import { useSourcePrefix } from '../core'; /** * Get the current (edited) value of the record from the form and pass it @@ -64,13 +65,17 @@ export const FormDataConsumerView = < >( props: Props ) => { - const { children, form, formData, source, index, ...rest } = props; + const { children, form, formData, source, ...rest } = props; let ret; + const prefix = useSourcePrefix(); + const matches = ArraySourceRegex.exec(prefix); + // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) - if (typeof index !== 'undefined' && source) { - const scopedFormData = get(formData, source); - const getSource = (scopedSource: string) => `${source}.${scopedSource}`; + if (matches && prefix) { + const scopedFormData = get(formData, prefix); + // Not needed anymore. Kept to avoid breaking existing code + const getSource = (scopedSource: string) => scopedSource; ret = children({ formData, scopedFormData, getSource, ...rest }); } else { ret = children({ @@ -85,6 +90,8 @@ export const FormDataConsumerView = < export default FormDataConsumer; +const ArraySourceRegex = new RegExp(/.+\.\d+$/); + export interface FormDataConsumerRenderParams< TFieldValues extends FieldValues = FieldValues, TScopedFieldValues extends FieldValues = TFieldValues diff --git a/packages/ra-core/src/form/useApplyInputDefaultValues.ts b/packages/ra-core/src/form/useApplyInputDefaultValues.ts index 89bf0132432..26fa132ec97 100644 --- a/packages/ra-core/src/form/useApplyInputDefaultValues.ts +++ b/packages/ra-core/src/form/useApplyInputDefaultValues.ts @@ -7,6 +7,7 @@ import { import get from 'lodash/get'; import { useRecordContext } from '../controller'; import { InputProps } from './useInput'; +import { useSourcePrefix } from '../core'; interface StandardInput { inputProps: Partial & { source: string }; @@ -32,6 +33,9 @@ export const useApplyInputDefaultValues = ({ fieldArrayInputControl, }: Props) => { const { defaultValue, source } = inputProps; + const prefix = useSourcePrefix(); + const finalSource = prefix ? `${prefix}.${source}` : source; + const record = useRecordContext(inputProps); const { getValues, @@ -41,8 +45,8 @@ export const useApplyInputDefaultValues = ({ reset, } = useFormContext(); const recordValue = get(record, source); - const formValue = get(getValues(), source); - const { isDirty } = getFieldState(source, formState); + const formValue = get(getValues(), finalSource); + const { isDirty } = getFieldState(finalSource, formState); useEffect(() => { if ( @@ -58,11 +62,11 @@ export const useApplyInputDefaultValues = ({ // Since we use get(record, source), if source is like foo.23.bar, // this effect will run. However we only want to set the default value // for the subfield bar if the record actually has a value for foo.23 - const pathContainsIndex = source + const pathContainsIndex = finalSource .split('.') .some(pathPart => numericRegex.test(pathPart)); if (pathContainsIndex) { - const parentPath = source.split('.').slice(0, -1).join('.'); + const parentPath = finalSource.split('.').slice(0, -1).join('.'); const parentValue = get(getValues(), parentPath); if (parentValue == null) { // the parent is undefined, so we don't want to set the default value @@ -88,7 +92,7 @@ export const useApplyInputDefaultValues = ({ return; } - resetField(source, { defaultValue }); + resetField(finalSource, { defaultValue }); }); }; diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index 85decdc4108..8596e413139 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -16,6 +16,7 @@ import { useFormGroupContext } from './useFormGroupContext'; import { useFormGroups } from './useFormGroups'; import { useApplyInputDefaultValues } from './useApplyInputDefaultValues'; import { useEvent } from '../util'; +import { useSourcePrefix } from '../core'; // replace null or undefined values by empty string to avoid controlled/uncontrolled input warning const defaultFormat = (value: any) => (value == null ? '' : value); @@ -38,7 +39,9 @@ export const useInput = ( validate, ...options } = props; - const finalName = name || source; + const prefix = useSourcePrefix(); + const finalSource = prefix ? `${prefix}.${source}` : source; + const finalName = name || finalSource; const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); const record = useRecordContext(); @@ -48,12 +51,12 @@ export const useInput = ( return; } - formGroups.registerField(source, formGroupName); + formGroups.registerField(finalSource, formGroupName); return () => { - formGroups.unregisterField(source, formGroupName); + formGroups.unregisterField(finalSource, formGroupName); }; - }, [formGroups, formGroupName, source]); + }, [formGroups, formGroupName, finalSource]); const sanitizedValidate = Array.isArray(validate) ? composeValidators(validate) @@ -120,7 +123,7 @@ export const useInput = ( }; return { - id: id || source, + id: id || finalSource, field, fieldState, formState, diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index e5cf2df744c..a9dfeb32ba3 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { ReactElement, memo } from 'react'; import { useTranslateLabel } from '../i18n'; +import { useSourcePrefix } from '../core'; export interface FieldTitleProps { isRequired?: boolean; @@ -12,6 +13,8 @@ export interface FieldTitleProps { export const FieldTitle = (props: FieldTitleProps) => { const { source, label, resource, isRequired } = props; + const prefix = useSourcePrefix(); + const finalSource = prefix ? `${prefix}.${source}` : source; const translateLabel = useTranslateLabel(); if (label === true) { @@ -33,7 +36,7 @@ export const FieldTitle = (props: FieldTitleProps) => { {translateLabel({ label, resource, - source, + source: finalSource, })} {isRequired && } diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index 393b1a4e605..0f014e00c7d 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { RecordContextProvider, + ResourceContextProvider, minLength, required, testDataProvider, @@ -104,22 +105,24 @@ describe('', () => { it('should clone each input once per value in the array', () => { render( - - - - - - - - + + + + + + + + + + ); expect( @@ -143,29 +146,30 @@ describe('', () => { it('should apply validation to both itself and its inner inputs', async () => { render( - - + - - - - - - + + + + + + + + ); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index c17726eee40..9c5e43a07b3 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -10,6 +10,7 @@ import { useGetValidationErrorMessage, useFormGroupContext, useFormGroups, + useSourcePrefix, } from 'ra-core'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { @@ -90,6 +91,8 @@ export const ArrayInput = (props: ArrayInputProps) => { const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); + const prefix = useSourcePrefix(); + const finalSource = prefix ? `${prefix}.${source}` : source; const sanitizedValidate = Array.isArray(validate) ? composeSyncValidators(validate) @@ -105,7 +108,7 @@ export const ArrayInput = (props: ArrayInputProps) => { } = useFormContext(); const fieldProps = useFieldArray({ - name: source, + name: finalSource, rules: { validate: async value => { if (!sanitizedValidate) return true; @@ -125,14 +128,14 @@ export const ArrayInput = (props: ArrayInputProps) => { // We need to register the array itself as a field to enable validation at its level useEffect(() => { - register(source); - formGroups.registerField(source, formGroupName); + register(finalSource); + formGroups.registerField(finalSource, formGroupName); return () => { - unregister(source, { keepValue: true }); - formGroups.unregisterField(source, formGroupName); + unregister(finalSource, { keepValue: true }); + formGroups.unregisterField(finalSource, formGroupName); }; - }, [register, unregister, source, formGroups, formGroupName]); + }, [register, unregister, finalSource, formGroups, formGroupName]); useApplyInputDefaultValues({ inputProps: props, @@ -140,7 +143,7 @@ export const ArrayInput = (props: ArrayInputProps) => { fieldArrayInputControl: fieldProps, }); - const { isDirty, error } = getFieldState(source, formState); + const { isDirty, error } = getFieldState(finalSource, formState); if (isPending) { return ( @@ -158,7 +161,7 @@ export const ArrayInput = (props: ArrayInputProps) => { margin={margin} className={clsx( 'ra-input', - `ra-input-${source}`, + `ra-input-${finalSource}`, ArrayInputClasses.root, className )} @@ -166,7 +169,7 @@ export const ArrayInput = (props: ArrayInputProps) => { {...sanitizeInputRestProps(rest)} > ', () => { expect((inputElements[1] as HTMLInputElement).value).toBe('bar'); }); - it('should render disabled inputs when disabled is true', () => { - render( - - - - - - - - - - ); - const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' - ); - expect(inputElements).toHaveLength(2); - expect((inputElements[0] as HTMLInputElement).disabled).toBeTruthy(); - expect((inputElements[0] as HTMLInputElement).value).toBe('foo'); - expect((inputElements[1] as HTMLInputElement).disabled).toBeTruthy(); - expect((inputElements[1] as HTMLInputElement).value).toBe('bar'); - }); - it('should allow to override the disabled prop of each inputs', () => { render( @@ -501,17 +474,14 @@ describe('', () => { - {({ getSource }) => ( + {() => ( <> - + )} diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index 747ace0564d..1cddd0b7799 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -1,17 +1,15 @@ import * as React from 'react'; import { - Children, cloneElement, MouseEvent, MouseEventHandler, - isValidElement, ReactElement, ReactNode, useMemo, } from 'react'; import { Typography } from '@mui/material'; import clsx from 'clsx'; -import { RaRecord } from 'ra-core'; +import { RaRecord, SourcePrefixContextProvider } from 'ra-core'; import { SimpleFormIteratorClasses } from './useSimpleFormIteratorStyles'; import { useSimpleFormIterator } from './useSimpleFormIterator'; @@ -35,7 +33,6 @@ export const SimpleFormIteratorItem = React.forwardRef( record, removeButton, reOrderButtons, - resource, source, } = props; @@ -94,24 +91,9 @@ export const SimpleFormIteratorItem = React.forwardRef( inline && SimpleFormIteratorClasses.inline )} > - {Children.map( - children, - (input: ReactElement, index2) => { - if (!isValidElement(input)) { - return null; - } - const { source, ...inputProps } = input.props; - return cloneElement(input, { - source: source - ? `${member}.${source}` - : member, - index: source ? undefined : index2, - resource, - disabled, - ...inputProps, - }); - } - )} + + {children} + {!disabled && ( From 8b09539143ffa67f4ce9e18327f963d592106522 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:50:50 +0100 Subject: [PATCH 02/28] Update upgrade guide --- docs/Upgrade.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 19d5258d998..156690c5780 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -204,6 +204,21 @@ const CompanyField = () => ( ``` {% endraw %} +## `` no longer clone its children + +One consequence is that defining the `disabled` prop on the `` component does not disable its children inputs anymore. If you relied on this behavior, you now have to specify the `disabled` prop on each input: + +```diff + + +- +- ++ ++ + + +``` + ## Upgrading to v4 If you are on react-admin v3, follow the [Upgrading to v4](https://marmelab.com/react-admin/doc/4.16/Upgrade.html) guide before upgrading to v5. From f782ab8a70ab6f6801ff8d4bff11d1b2be4e8354 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:41:09 +0100 Subject: [PATCH 03/28] Refactor to handle TranslatableInput --- docs/Inputs.md | 10 ++--- docs/SimpleFormIterator.md | 8 ++-- docs/Upgrade.md | 4 +- examples/simple/src/posts/PostCreate.tsx | 4 +- examples/simple/src/posts/PostEdit.tsx | 4 +- .../ra-core/src/core/SourcePrefixContext.tsx | 19 ++++----- .../src/form/FormDataConsumer.spec.tsx | 12 ++---- .../ra-core/src/form/FormDataConsumer.tsx | 21 ++++------ .../src/form/useApplyInputDefaultValues.ts | 5 +-- packages/ra-core/src/form/useInput.ts | 7 ++-- packages/ra-core/src/util/FieldTitle.tsx | 14 +++---- .../src/input/ArrayInput/ArrayInput.spec.tsx | 12 +++--- .../src/input/ArrayInput/ArrayInput.tsx | 7 ++-- .../ArrayInput/SimpleFormIterator.spec.tsx | 39 +++++++------------ .../ArrayInput/SimpleFormIteratorItem.tsx | 11 ++++-- .../src/input/SelectArrayInput.stories.tsx | 27 +++++-------- .../src/input/TranslatableInputs.spec.tsx | 4 +- .../input/TranslatableInputsTabContent.tsx | 30 +++++--------- 18 files changed, 96 insertions(+), 142 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index d83e2bf2c91..bded75ea8c4 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -537,9 +537,8 @@ const OrderEdit = () => ( **Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide two additional properties to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the `ArrayInput` -- `getSource`: a function that translates the source into a valid one for the `ArrayInput` -And here is an example usage for `getSource` inside ``: +And here is an example usage for `scopedFormData` inside ``: ```tsx import { FormDataConsumer } from 'react-admin'; @@ -554,12 +553,11 @@ const PostEdit = () => ( {({ formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput - getSource, // A function to get the valid source inside an ArrayInput ...rest }) => - scopedFormData && getSource && scopedFormData.name ? ( + scopedFormData && scopedFormData.name ? ( @@ -573,7 +571,7 @@ const PostEdit = () => ( ); ``` -**Tip:** TypeScript users will notice that `scopedFormData` and `getSource` are typed as optional parameters. This is because the `` component can be used outside of an `` and in that case, these parameters will be `undefined`. If you are inside an ``, you can safely assume that these parameters will be defined. +**Tip:** TypeScript users will notice that `scopedFormData` is typed as optional parameters. This is because the `` component can be used outside of an `` and in that case, these parameters will be `undefined`. If you are inside an ``, you can safely assume that these parameters will be defined. ## Hiding Inputs Based On Other Inputs diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 8fa004b39e1..7d862f57523 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -117,9 +117,8 @@ By default, `` renders one input per line, but they can be d `` also accepts `` as child. When used inside a form iterator, `` provides two additional properties to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the ArrayInput -- `getSource`: a function that translates the source into a valid one for the ArrayInput -And here is an example usage for `getSource` inside ``: +And here is an example usage for `scopedFormData` inside ``: ```jsx import { FormDataConsumer } from 'react-admin'; @@ -134,12 +133,11 @@ const PostEdit = () => ( {({ formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput - getSource, // A function to get the valid source inside an ArrayInput ...rest }) => scopedFormData && scopedFormData.name ? ( @@ -153,7 +151,7 @@ const PostEdit = () => ( ); ``` -**Tip:** TypeScript users will notice that `scopedFormData` and `getSource` are typed as optional parameters. This is because the `` component can be used outside of a `` and in that case, these parameters will be `undefined`. If you are inside a ``, you can safely assume that these parameters will be defined. +**Tip:** TypeScript users will notice that `scopedFormData` is typed as optional parameters. This is because the `` component can be used outside of a `` and in that case, these parameters will be `undefined`. If you are inside a ``, you can safely assume that these parameters will be defined. **Note**: `` only accepts `Input` components as children. If you want to use some `Fields` instead, you have to use a ``, as follows: diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 156690c5780..d84e0f6a03f 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -204,9 +204,9 @@ const CompanyField = () => ( ``` {% endraw %} -## `` no longer clone its children +## `` no longer clones its children -One consequence is that defining the `disabled` prop on the `` component does not disable its children inputs anymore. If you relied on this behavior, you now have to specify the `disabled` prop on each input: +We've changed the implementation of ``, the companion child of ``. This internal change is mostly backwards compatible, with one exception: defining the `disabled` prop on the `` component does not disable the children inputs anymore. If you relied on this behavior, you now have to specify the `disabled` prop on each input: ```diff diff --git a/examples/simple/src/posts/PostCreate.tsx b/examples/simple/src/posts/PostCreate.tsx index f8acc6ee04e..2aef45fb01c 100644 --- a/examples/simple/src/posts/PostCreate.tsx +++ b/examples/simple/src/posts/PostCreate.tsx @@ -163,10 +163,10 @@ const PostCreate = () => { /> - {({ scopedFormData, getSource, ...rest }) => + {({ scopedFormData, ...rest }) => scopedFormData && scopedFormData.user_id ? ( { - {({ scopedFormData, getSource, ...rest }) => + {({ scopedFormData, ...rest }) => scopedFormData && scopedFormData.user_id ? ( string; /** - * Context to that provides a possible prefix for the source prop of fields and inputs. + * Context to that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. */ -export const SourcePrefixContext = createContext(''); +export const SourceContext = createContext(null); -export const SourcePrefixContextProvider = SourcePrefixContext.Provider; +export const SourceContextProvider = SourceContext.Provider; /** - * Hook to get the source prefix that a field or input should add to its source prop. - * @returns The source prefix that a field or input should add to its source prop. + * Hook to get a source that may be prefixed, suffixed, etc. by a parent component. + * @param source The original field or input source + * @returns The modified source */ -export const useSourcePrefix = (): string => { - const prefix = useContext(SourcePrefixContext); - return prefix ?? ''; +export const useWrappedSource = (source: string) => { + const context = useContext(SourceContext); + return context ? context(source) : source; }; diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index faf56671a29..50a229dc189 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -30,7 +30,6 @@ describe('FormDataConsumerView', () => { expect(children).toHaveBeenCalledWith({ formData, - getSource: expect.anything(), }); }); @@ -41,7 +40,7 @@ describe('FormDataConsumerView', () => { - {({ formData, getSource, ...rest }) => { + {({ formData, ...rest }) => { globalFormData = formData; return ; @@ -96,12 +95,7 @@ describe('FormDataConsumerView', () => { - {({ - formData, - scopedFormData, - getSource, - ...rest - }) => { + {({ formData, scopedFormData, ...rest }) => { globalScopedFormData = scopedFormData; return scopedFormData && scopedFormData.name ? ( @@ -122,7 +116,7 @@ describe('FormDataConsumerView', () => { expect(globalScopedFormData).toEqual({ name: null }); fireEvent.change( - screen.getByLabelText('resources.undefined.fields.authors.name'), + screen.getByLabelText('resources.undefined.fields.name'), { target: { value: 'a' }, } diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index b3e6ea6fbf5..eeba567c459 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import { useFormContext, FieldValues } from 'react-hook-form'; import get from 'lodash/get'; import { useFormValues } from './useFormValues'; -import { useSourcePrefix } from '../core'; +import { useWrappedSource } from '../core'; /** * Get the current (edited) value of the record from the form and pass it @@ -68,21 +68,15 @@ export const FormDataConsumerView = < const { children, form, formData, source, ...rest } = props; let ret; - const prefix = useSourcePrefix(); - const matches = ArraySourceRegex.exec(prefix); + const finalSource = useWrappedSource(''); + const matches = ArraySourceRegex.exec(finalSource); // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) - if (matches && prefix) { - const scopedFormData = get(formData, prefix); - // Not needed anymore. Kept to avoid breaking existing code - const getSource = (scopedSource: string) => scopedSource; - ret = children({ formData, scopedFormData, getSource, ...rest }); + if (matches) { + const scopedFormData = get(formData, matches[0]); + ret = children({ formData, scopedFormData, ...rest }); } else { - ret = children({ - formData, - getSource: (scopedSource: string) => scopedSource, - ...rest, - }); + ret = children({ formData, ...rest }); } return ret === undefined ? null : ret; @@ -98,7 +92,6 @@ export interface FormDataConsumerRenderParams< > { formData: TFieldValues; scopedFormData?: TScopedFieldValues; - getSource: (source: string) => string; } export type FormDataConsumerRender< diff --git a/packages/ra-core/src/form/useApplyInputDefaultValues.ts b/packages/ra-core/src/form/useApplyInputDefaultValues.ts index 26fa132ec97..336f9e44cd2 100644 --- a/packages/ra-core/src/form/useApplyInputDefaultValues.ts +++ b/packages/ra-core/src/form/useApplyInputDefaultValues.ts @@ -7,7 +7,7 @@ import { import get from 'lodash/get'; import { useRecordContext } from '../controller'; import { InputProps } from './useInput'; -import { useSourcePrefix } from '../core'; +import { useWrappedSource } from '../core'; interface StandardInput { inputProps: Partial & { source: string }; @@ -33,8 +33,7 @@ export const useApplyInputDefaultValues = ({ fieldArrayInputControl, }: Props) => { const { defaultValue, source } = inputProps; - const prefix = useSourcePrefix(); - const finalSource = prefix ? `${prefix}.${source}` : source; + const finalSource = useWrappedSource(source); const record = useRecordContext(inputProps); const { diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index 8596e413139..f15413e0d22 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -16,7 +16,7 @@ import { useFormGroupContext } from './useFormGroupContext'; import { useFormGroups } from './useFormGroups'; import { useApplyInputDefaultValues } from './useApplyInputDefaultValues'; import { useEvent } from '../util'; -import { useSourcePrefix } from '../core'; +import { useWrappedSource } from '../core'; // replace null or undefined values by empty string to avoid controlled/uncontrolled input warning const defaultFormat = (value: any) => (value == null ? '' : value); @@ -39,8 +39,7 @@ export const useInput = ( validate, ...options } = props; - const prefix = useSourcePrefix(); - const finalSource = prefix ? `${prefix}.${source}` : source; + const finalSource = useWrappedSource(source); const finalName = name || finalSource; const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); @@ -68,7 +67,7 @@ export const useInput = ( // (i.e. field level defaultValue override form level defaultValues for this field). const { field: controllerField, fieldState, formState } = useController({ name: finalName, - defaultValue: get(record, source, defaultValue), + defaultValue: get(record, finalSource, defaultValue), rules: { validate: async (value, values) => { if (!sanitizedValidate) return true; diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index a9dfeb32ba3..80eeb96d5df 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { ReactElement, memo } from 'react'; import { useTranslateLabel } from '../i18n'; -import { useSourcePrefix } from '../core'; export interface FieldTitleProps { isRequired?: boolean; @@ -13,8 +12,6 @@ export interface FieldTitleProps { export const FieldTitle = (props: FieldTitleProps) => { const { source, label, resource, isRequired } = props; - const prefix = useSourcePrefix(); - const finalSource = prefix ? `${prefix}.${source}` : source; const translateLabel = useTranslateLabel(); if (label === true) { @@ -31,13 +28,14 @@ export const FieldTitle = (props: FieldTitleProps) => { return label; } + const translatedLabel = translateLabel({ + label, + resource, + source, + }); return ( - {translateLabel({ - label, - resource, - source: finalSource, - })} + {translatedLabel} {isRequired && } ); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index 0f014e00c7d..eb8efced8de 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -126,19 +126,19 @@ describe('', () => { ); expect( - screen.queryAllByLabelText('resources.bar.fields.arr.id') + screen.queryAllByLabelText('resources.bar.fields.id') ).toHaveLength(2); expect( screen - .queryAllByLabelText('resources.bar.fields.arr.id') + .queryAllByLabelText('resources.bar.fields.id') .map(input => (input as HTMLInputElement).value) ).toEqual(['123', '456']); expect( - screen.queryAllByLabelText('resources.bar.fields.arr.foo') + screen.queryAllByLabelText('resources.bar.fields.foo') ).toHaveLength(2); expect( screen - .queryAllByLabelText('resources.bar.fields.arr.foo') + .queryAllByLabelText('resources.bar.fields.foo') .map(input => (input as HTMLInputElement).value) ).toEqual(['bar', 'baz']); }); @@ -180,7 +180,7 @@ describe('', () => { }); fireEvent.click(screen.getByLabelText('ra.action.add')); const firstId = screen.getAllByLabelText( - 'resources.bar.fields.arr.id *' + 'resources.bar.fields.id *' )[0]; fireEvent.change(firstId, { target: { value: 'aaa' }, @@ -190,7 +190,7 @@ describe('', () => { }); fireEvent.blur(firstId); const firstFoo = screen.getAllByLabelText( - 'resources.bar.fields.arr.foo *' + 'resources.bar.fields.foo *' )[0]; fireEvent.change(firstFoo, { target: { value: 'aaa' }, diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index 9c5e43a07b3..cf2a2d757f8 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -10,7 +10,7 @@ import { useGetValidationErrorMessage, useFormGroupContext, useFormGroups, - useSourcePrefix, + useWrappedSource, } from 'ra-core'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { @@ -91,8 +91,7 @@ export const ArrayInput = (props: ArrayInputProps) => { const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); - const prefix = useSourcePrefix(); - const finalSource = prefix ? `${prefix}.${source}` : source; + const finalSource = useWrappedSource(source); const sanitizedValidate = Array.isArray(validate) ? composeSyncValidators(validate) @@ -161,7 +160,7 @@ export const ArrayInput = (props: ArrayInputProps) => { margin={margin} className={clsx( 'ra-input', - `ra-input-${finalSource}`, + `ra-input-${source}`, ArrayInputClasses.root, className )} diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index b84fd42b0de..49265b4c8d4 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -48,7 +48,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ); expect(inputElements).toHaveLength(2); expect((inputElements[0] as HTMLInputElement).disabled).toBeFalsy(); @@ -75,7 +75,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ); expect(inputElements).toHaveLength(2); expect((inputElements[0] as HTMLInputElement).disabled).toBeTruthy(); @@ -224,7 +224,7 @@ describe('', () => { fireEvent.click(screen.getByText('ra.action.confirm')); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ); expect(inputElements.length).toBe(0); }); @@ -250,7 +250,7 @@ describe('', () => { fireEvent.click(addItemElement); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ); expect(inputElements.length).toBe(1); @@ -259,14 +259,14 @@ describe('', () => { fireEvent.click(addItemElement); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ); expect(inputElements.length).toBe(2); }); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -542,7 +542,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -560,7 +560,7 @@ describe('', () => { fireEvent.click(removeFirstButton); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -587,7 +587,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -603,7 +603,7 @@ describe('', () => { fireEvent.click(moveDownFirstButton[0]); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -618,7 +618,7 @@ describe('', () => { fireEvent.click(moveUpButton[1]); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.emails.email' + 'resources.undefined.fields.email' ) as HTMLInputElement[]; expect( @@ -834,13 +834,9 @@ describe('', () => { - {({ scopedFormData, getSource }) => + {({ scopedFormData }) => scopedFormData && scopedFormData.name ? ( - string)('role')} - /> + ) : null } @@ -881,14 +877,7 @@ describe('', () => { - {({ getSource }) => ( - string)('role')} - /> - )} + {() => } diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index 1cddd0b7799..a71b9804b8a 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -9,7 +9,7 @@ import { } from 'react'; import { Typography } from '@mui/material'; import clsx from 'clsx'; -import { RaRecord, SourcePrefixContextProvider } from 'ra-core'; +import { RaRecord, SourceContextProvider } from 'ra-core'; import { SimpleFormIteratorClasses } from './useSimpleFormIteratorStyles'; import { useSimpleFormIterator } from './useSimpleFormIterator'; @@ -74,6 +74,11 @@ export const SimpleFormIteratorItem = React.forwardRef( ? getItemLabel(index) : getItemLabel; + const sourceContext = useMemo( + () => (source: string) => (source ? `${member}.${source}` : member), + [member] + ); + return (
  • @@ -91,9 +96,9 @@ export const SimpleFormIteratorItem = React.forwardRef( inline && SimpleFormIteratorClasses.inline )} > - + {children} - + {!disabled && ( diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index 0f460c57146..7134300c7a8 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -112,24 +112,15 @@ export const InsideArrayInput = () => ( defaultValue={[{ data: ['foo'] }]} > - - {({ getSource }) => { - const source = getSource!('data'); - return ( - <> - - - ); - }} - + diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx index 3e2eafc03fc..3628f58292a 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx @@ -163,11 +163,11 @@ describe('', () => { ); - fireEvent.change(screen.queryByDisplayValue('english name'), { + fireEvent.change(screen.getByDisplayValue('english name'), { target: { value: 'english name updated' }, }); fireEvent.click(screen.getByText('ra.locales.fr')); - fireEvent.change(screen.queryByDisplayValue('french nested field'), { + fireEvent.change(screen.getByDisplayValue('french nested field'), { target: { value: 'french nested field updated' }, }); fireEvent.click(screen.getByText('ra.action.save')); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index be486333742..6ffdad1e74b 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -2,16 +2,11 @@ import * as React from 'react'; import { styled } from '@mui/material/styles'; import { Stack, StackProps } from '@mui/material'; import clsx from 'clsx'; -import { - Children, - cloneElement, - isValidElement, - ReactElement, - ReactNode, -} from 'react'; +import { ReactElement, ReactNode, useMemo } from 'react'; import { FormGroupContextProvider, RaRecord, + SourceContextProvider, useTranslatableContext, } from 'ra-core'; @@ -23,7 +18,11 @@ export const TranslatableInputsTabContent = ( props: TranslatableInputsTabContentProps ): ReactElement => { const { children, groupKey = '', locale, ...other } = props; - const { selectedLocale, getLabel, getSource } = useTranslatableContext(); + const { selectedLocale, getSource } = useTranslatableContext(); + const sourceContext = useMemo( + () => (source: string) => getSource(source, locale), + [getSource, locale] + ); return ( @@ -37,18 +36,9 @@ export const TranslatableInputsTabContent = ( })} {...other} > - {Children.map(children, child => - isValidElement(child) - ? cloneElement(child, { - ...child.props, - label: getLabel( - child.props.source, - child.props.label - ), - source: getSource(child.props.source, locale), - }) - : null - )} + + {children} + ); From 73965786dda9b40584f4f5d3fcba0712db0b3fad Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 20 Dec 2023 11:38:11 +0100 Subject: [PATCH 04/28] Apply suggestions from code review Co-authored-by: adrien guernier --- docs/Inputs.md | 2 +- docs/SimpleFormIterator.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index bded75ea8c4..a56f11b4bf4 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -534,7 +534,7 @@ const OrderEdit = () => ( ); ``` -**Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide two additional properties to its children function: +**Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide one additional propertie to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the `ArrayInput` diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 7d862f57523..381cae98125 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -114,7 +114,7 @@ A list of Input elements, that will be rendered on each row. By default, `` renders one input per line, but they can be displayed inline with the `inline` prop. -`` also accepts `` as child. When used inside a form iterator, `` provides two additional properties to its children function: +`` also accepts `` as child. When used inside a form iterator, `` provides one additional propertie to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the ArrayInput From e00521cc48b4d3e57b7559b9d783b6712fc8fda0 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 20 Dec 2023 11:51:16 +0100 Subject: [PATCH 05/28] Apply suggestions from code review Co-authored-by: adrien guernier --- packages/ra-core/src/core/SourcePrefixContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/core/SourcePrefixContext.tsx b/packages/ra-core/src/core/SourcePrefixContext.tsx index 19b753f5ddd..43311208410 100644 --- a/packages/ra-core/src/core/SourcePrefixContext.tsx +++ b/packages/ra-core/src/core/SourcePrefixContext.tsx @@ -3,7 +3,7 @@ import { createContext, useContext } from 'react'; export type SourceContextValue = (source: string) => string; /** - * Context to that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. + * Context that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. */ export const SourceContext = createContext(null); From c954747ca6095ed4012f2d4e3901fc16521a71d3 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:57:42 +0100 Subject: [PATCH 06/28] Apply suggestions from code review Co-authored-by: adrien guernier --- docs/Inputs.md | 2 +- docs/SimpleFormIterator.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index a56f11b4bf4..a3cbfec412b 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -534,7 +534,7 @@ const OrderEdit = () => ( ); ``` -**Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide one additional propertie to its children function: +**Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide one additional property to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the `ArrayInput` diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 381cae98125..bc9e8634ce6 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -114,7 +114,7 @@ A list of Input elements, that will be rendered on each row. By default, `` renders one input per line, but they can be displayed inline with the `inline` prop. -`` also accepts `` as child. When used inside a form iterator, `` provides one additional propertie to its children function: +`` also accepts `` as child. When used inside a form iterator, `` provides one additional property to its children function: - `scopedFormData`: an object containing the current values of the currently rendered item from the ArrayInput From 7d14f1d666622136eb1fbba84575a9f5db2b353d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:00:38 +0100 Subject: [PATCH 07/28] Linting --- .../ra-ui-materialui/src/input/SelectArrayInput.stories.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index 7134300c7a8..c0d7aa60e8b 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -9,6 +9,7 @@ import { TextField, } from '@mui/material'; import fakeRestProvider from 'ra-data-fakerest'; +import { useWatch } from 'react-hook-form'; import { AdminContext } from '../AdminContext'; import { Create, Edit } from '../detail'; @@ -18,8 +19,6 @@ import { ReferenceArrayInput } from './ReferenceArrayInput'; import { useCreateSuggestionContext } from './useSupportCreateSuggestion'; import { TextInput } from './TextInput'; import { ArrayInput, SimpleFormIterator } from './ArrayInput'; -import { FormDataConsumer } from 'ra-core'; -import { useWatch } from 'react-hook-form'; export default { title: 'ra-ui-materialui/input/SelectArrayInput' }; From 6dd99fcfdae141192cf4bfb8872a8c694f85bc87 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 3 Jan 2024 15:40:28 +0100 Subject: [PATCH 08/28] Rewriting --- docs/Inputs.md | 6 +----- docs/SimpleFormIterator.md | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index a3cbfec412b..9a7a43c4e4e 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -534,11 +534,7 @@ const OrderEdit = () => ( ); ``` -**Tip**: When using a `FormDataConsumer` inside an `ArrayInput`, the `FormDataConsumer` will provide one additional property to its children function: - -- `scopedFormData`: an object containing the current values of the currently rendered item from the `ArrayInput` - -And here is an example usage for `scopedFormData` inside ``: +**Tip**: When used inside an `ArrayInput`, `` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a ``, as in the following example: ```tsx import { FormDataConsumer } from 'react-admin'; diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index bc9e8634ce6..32b1e3e337d 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -114,11 +114,7 @@ A list of Input elements, that will be rendered on each row. By default, `` renders one input per line, but they can be displayed inline with the `inline` prop. -`` also accepts `` as child. When used inside a form iterator, `` provides one additional property to its children function: - -- `scopedFormData`: an object containing the current values of the currently rendered item from the ArrayInput - -And here is an example usage for `scopedFormData` inside ``: +`` also accepts `` as child. In this case, `` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a ``, as in the following example: ```jsx import { FormDataConsumer } from 'react-admin'; From 4484f601f15c14d52bf140241b0e00fc1243fca6 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:35:36 +0100 Subject: [PATCH 09/28] Add an upgrade guide section about FormDataConsumer --- docs/Upgrade.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index d84e0f6a03f..606b13b8b6e 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -219,6 +219,43 @@ We've changed the implementation of ``, the companion child ``` +## `` no longer pass a `getSource` function + +As we introduced the `SourceContext`, we don't need the `getSource` function anymore when using the `` inside an ``: + +```diff +import { FormDataConsumer } from 'react-admin'; + +const PostEdit = () => ( + + + + + + > + {({ + formData, // The whole form data + scopedFormData, // The data for this item of the ArrayInput +- getSource, // A function to get the valid source inside an ArrayInput + ...rest + }) => + scopedFormData && getSource && scopedFormData.name ? ( + + ) : null + } + + + + + +); +``` + ## Upgrading to v4 If you are on react-admin v3, follow the [Upgrading to v4](https://marmelab.com/react-admin/doc/4.16/Upgrade.html) guide before upgrading to v5. From f56bb8cddd45feca62e71ba815c957d0968218f3 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:35:42 +0100 Subject: [PATCH 10/28] Revert FieldTitle unnecessary change --- packages/ra-core/src/util/FieldTitle.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index 80eeb96d5df..e5cf2df744c 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -28,14 +28,13 @@ export const FieldTitle = (props: FieldTitleProps) => { return label; } - const translatedLabel = translateLabel({ - label, - resource, - source, - }); return ( - {translatedLabel} + {translateLabel({ + label, + resource, + source, + })} {isRequired && } ); From 2ad60b395dd7dd1df2d62fdbab7ec1a24b8ff347 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:36:40 +0100 Subject: [PATCH 11/28] Rename SourceContext file and extract useWrappedSource --- packages/ra-core/src/core/SourceContext.tsx | 20 +++++++++++++++++++ .../ra-core/src/core/SourcePrefixContext.tsx | 20 ------------------- packages/ra-core/src/core/index.ts | 3 ++- packages/ra-core/src/core/useWrappedSource.ts | 18 +++++++++++++++++ 4 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 packages/ra-core/src/core/SourceContext.tsx delete mode 100644 packages/ra-core/src/core/SourcePrefixContext.tsx create mode 100644 packages/ra-core/src/core/useWrappedSource.ts diff --git a/packages/ra-core/src/core/SourceContext.tsx b/packages/ra-core/src/core/SourceContext.tsx new file mode 100644 index 00000000000..816c4e9cf5e --- /dev/null +++ b/packages/ra-core/src/core/SourceContext.tsx @@ -0,0 +1,20 @@ +import { createContext } from 'react'; + +export type SourceContextValue = (source: string) => string; + +/** + * Context that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. + * + * @example + * const CoordinatesInput = props => { + * return ( + * `coordinates.${source}`}> + * + * + * + * ); + * }; + */ +export const SourceContext = createContext(null); + +export const SourceContextProvider = SourceContext.Provider; diff --git a/packages/ra-core/src/core/SourcePrefixContext.tsx b/packages/ra-core/src/core/SourcePrefixContext.tsx deleted file mode 100644 index 43311208410..00000000000 --- a/packages/ra-core/src/core/SourcePrefixContext.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createContext, useContext } from 'react'; - -export type SourceContextValue = (source: string) => string; - -/** - * Context that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. - */ -export const SourceContext = createContext(null); - -export const SourceContextProvider = SourceContext.Provider; - -/** - * Hook to get a source that may be prefixed, suffixed, etc. by a parent component. - * @param source The original field or input source - * @returns The modified source - */ -export const useWrappedSource = (source: string) => { - const context = useContext(SourceContext); - return context ? context(source) : source; -}; diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts index b8bfc93593d..f92012eff85 100644 --- a/packages/ra-core/src/core/index.ts +++ b/packages/ra-core/src/core/index.ts @@ -13,4 +13,5 @@ export * from './useResourceContext'; export * from './useResourceDefinition'; export * from './useResourceDefinitions'; export * from './useGetRecordRepresentation'; -export * from './SourcePrefixContext'; +export * from './SourceContext'; +export * from './useWrappedSource'; diff --git a/packages/ra-core/src/core/useWrappedSource.ts b/packages/ra-core/src/core/useWrappedSource.ts new file mode 100644 index 00000000000..a1808d9e0c5 --- /dev/null +++ b/packages/ra-core/src/core/useWrappedSource.ts @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import { SourceContext } from './SourceContext'; + +/** + * Hook to get a source that may be prefixed, suffixed, etc. by a parent component. You don't need to call this hook if you use the `useInput` hook. + * @param source The original field or input source + * @returns The modified source if the calling component is inside a `SourceContext`, the original source otherwise. + * + * @example + * const MyInput = ({ source, ...props }) => { + * const finalSource = useWrappedSource(source); + * return ; + * }; + */ +export const useWrappedSource = (source: string) => { + const context = useContext(SourceContext); + return context ? context(source) : source; +}; From cfaf0180f332c269b79e715c2650c0f8092fc216 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:55:33 +0100 Subject: [PATCH 12/28] Revert breaking changes for labels --- packages/ra-core/src/core/SourceContext.tsx | 4 +++- .../src/form/FormDataConsumer.spec.tsx | 4 ++-- packages/ra-core/src/util/FieldTitle.tsx | 4 +++- .../src/input/ArrayInput/ArrayInput.spec.tsx | 12 +++++----- .../ArrayInput/SimpleFormIterator.spec.tsx | 22 +++++++++---------- .../src/input/TranslatableInputs.spec.tsx | 6 ++--- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/ra-core/src/core/SourceContext.tsx b/packages/ra-core/src/core/SourceContext.tsx index 816c4e9cf5e..f514fb9ba2c 100644 --- a/packages/ra-core/src/core/SourceContext.tsx +++ b/packages/ra-core/src/core/SourceContext.tsx @@ -1,4 +1,4 @@ -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; export type SourceContextValue = (source: string) => string; @@ -18,3 +18,5 @@ export type SourceContextValue = (source: string) => string; export const SourceContext = createContext(null); export const SourceContextProvider = SourceContext.Provider; + +export const useSourceContext = () => useContext(SourceContext); diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 50a229dc189..6aeb84d80fc 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -14,7 +14,7 @@ import { import expect from 'expect'; describe('FormDataConsumerView', () => { - it('does not call its children function with scopedFormData and getSource if it did not receive an index prop', () => { + it('does not call its children function with scopedFormData if it did not receive an index prop', () => { const children = jest.fn(); const formData = { id: 123, title: 'A title' }; @@ -116,7 +116,7 @@ describe('FormDataConsumerView', () => { expect(globalScopedFormData).toEqual({ name: null }); fireEvent.change( - screen.getByLabelText('resources.undefined.fields.name'), + screen.getByLabelText('resources.undefined.fields.authors.name'), { target: { value: 'a' }, } diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index e5cf2df744c..01c33275210 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { ReactElement, memo } from 'react'; import { useTranslateLabel } from '../i18n'; +import { useWrappedSource } from '../core'; export interface FieldTitleProps { isRequired?: boolean; @@ -13,6 +14,7 @@ export interface FieldTitleProps { export const FieldTitle = (props: FieldTitleProps) => { const { source, label, resource, isRequired } = props; const translateLabel = useTranslateLabel(); + const finalSource = useWrappedSource(source); if (label === true) { throw new Error( @@ -33,7 +35,7 @@ export const FieldTitle = (props: FieldTitleProps) => { {translateLabel({ label, resource, - source, + source: finalSource, })} {isRequired && } diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx index eb8efced8de..0f014e00c7d 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.spec.tsx @@ -126,19 +126,19 @@ describe('', () => { ); expect( - screen.queryAllByLabelText('resources.bar.fields.id') + screen.queryAllByLabelText('resources.bar.fields.arr.id') ).toHaveLength(2); expect( screen - .queryAllByLabelText('resources.bar.fields.id') + .queryAllByLabelText('resources.bar.fields.arr.id') .map(input => (input as HTMLInputElement).value) ).toEqual(['123', '456']); expect( - screen.queryAllByLabelText('resources.bar.fields.foo') + screen.queryAllByLabelText('resources.bar.fields.arr.foo') ).toHaveLength(2); expect( screen - .queryAllByLabelText('resources.bar.fields.foo') + .queryAllByLabelText('resources.bar.fields.arr.foo') .map(input => (input as HTMLInputElement).value) ).toEqual(['bar', 'baz']); }); @@ -180,7 +180,7 @@ describe('', () => { }); fireEvent.click(screen.getByLabelText('ra.action.add')); const firstId = screen.getAllByLabelText( - 'resources.bar.fields.id *' + 'resources.bar.fields.arr.id *' )[0]; fireEvent.change(firstId, { target: { value: 'aaa' }, @@ -190,7 +190,7 @@ describe('', () => { }); fireEvent.blur(firstId); const firstFoo = screen.getAllByLabelText( - 'resources.bar.fields.foo *' + 'resources.bar.fields.arr.foo *' )[0]; fireEvent.change(firstFoo, { target: { value: 'aaa' }, diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index 49265b4c8d4..e4bb6db41e4 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -48,7 +48,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ); expect(inputElements).toHaveLength(2); expect((inputElements[0] as HTMLInputElement).disabled).toBeFalsy(); @@ -75,7 +75,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ); expect(inputElements).toHaveLength(2); expect((inputElements[0] as HTMLInputElement).disabled).toBeTruthy(); @@ -224,7 +224,7 @@ describe('', () => { fireEvent.click(screen.getByText('ra.action.confirm')); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ); expect(inputElements.length).toBe(0); }); @@ -250,7 +250,7 @@ describe('', () => { fireEvent.click(addItemElement); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ); expect(inputElements.length).toBe(1); @@ -259,14 +259,14 @@ describe('', () => { fireEvent.click(addItemElement); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ); expect(inputElements.length).toBe(2); }); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( @@ -542,7 +542,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( @@ -560,7 +560,7 @@ describe('', () => { fireEvent.click(removeFirstButton); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( @@ -587,7 +587,7 @@ describe('', () => { ); const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( @@ -603,7 +603,7 @@ describe('', () => { fireEvent.click(moveDownFirstButton[0]); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( @@ -618,7 +618,7 @@ describe('', () => { fireEvent.click(moveUpButton[1]); await waitFor(() => { const inputElements = screen.queryAllByLabelText( - 'resources.undefined.fields.email' + 'resources.undefined.fields.emails.email' ) as HTMLInputElement[]; expect( diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx index 3628f58292a..ea314832f05 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx @@ -126,17 +126,17 @@ describe('', () => { ); fireEvent.change( - screen.getAllByLabelText('resources.undefined.fields.name')[0], + screen.getByLabelText('resources.undefined.fields.name.en'), { target: { value: 'english value' }, } ); fireEvent.click(screen.getByText('ra.locales.fr')); fireEvent.focus( - screen.getAllByLabelText('resources.undefined.fields.name')[1] + screen.getByLabelText('resources.undefined.fields.name.fr') ); fireEvent.blur( - screen.getAllByLabelText('resources.undefined.fields.name')[1] + screen.getByLabelText('resources.undefined.fields.name.fr') ); await waitFor(() => { expect(screen.queryByText('error')).not.toBeNull(); From 04ddc0eec44b37e7fb1fa53b7d139124ba4a8054 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:09:31 +0100 Subject: [PATCH 13/28] Make test title clearer --- packages/ra-core/src/form/FormDataConsumer.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 6aeb84d80fc..72c363d708b 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -14,7 +14,7 @@ import { import expect from 'expect'; describe('FormDataConsumerView', () => { - it('does not call its children function with scopedFormData if it did not receive an index prop', () => { + it('does not call its children function with scopedFormData if it did not receive a source containing an index', () => { const children = jest.fn(); const formData = { id: 123, title: 'A title' }; From ba8ffca0d3a0dc1be3a6737fc0f908072dd93791 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:03:35 +0100 Subject: [PATCH 14/28] Apply reviews suggestions --- docs/SimpleFormIterator.md | 2 +- docs/Upgrade.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 32b1e3e337d..8c6e7761f90 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -133,7 +133,7 @@ const PostEdit = () => ( }) => scopedFormData && scopedFormData.name ? ( diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 606b13b8b6e..a129ced2c86 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -224,7 +224,7 @@ We've changed the implementation of ``, the companion child As we introduced the `SourceContext`, we don't need the `getSource` function anymore when using the `` inside an ``: ```diff -import { FormDataConsumer } from 'react-admin'; +import { Edit, SimpleForm, TextInput, ArrayInput, SelectInput, FormDataConsumer } from 'react-admin'; const PostEdit = () => ( @@ -232,7 +232,7 @@ const PostEdit = () => ( - > + {({ formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput From 6244870dfc4094e724067e12d7ec9bb65e1b27af Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:03:52 +0100 Subject: [PATCH 15/28] Refactor to handle labels correctly --- packages/ra-core/src/core/SourceContext.tsx | 19 ++- packages/ra-core/src/core/index.ts | 1 - packages/ra-core/src/core/useWrappedSource.ts | 18 -- .../ra-core/src/form/FormDataConsumer.tsx | 9 +- .../src/form/useApplyInputDefaultValues.ts | 7 +- packages/ra-core/src/form/useInput.ts | 5 +- .../src/i18n/TestTranslationProvider.tsx | 1 - .../src/i18n/useTranslateLabel.spec.tsx | 155 ++++++++++++++++++ .../ra-core/src/i18n/useTranslateLabel.ts | 29 +++- packages/ra-core/src/util/FieldTitle.tsx | 4 +- .../util/getFieldLabelTranslationArgs.spec.ts | 10 -- .../src/util/getFieldLabelTranslationArgs.ts | 6 +- .../src/input/ArrayInput/ArrayInput.tsx | 5 +- .../ArrayInput/SimpleFormIteratorItem.tsx | 18 +- .../src/input/TranslatableInputs.spec.tsx | 6 +- .../input/TranslatableInputsTabContent.tsx | 9 +- 16 files changed, 238 insertions(+), 64 deletions(-) delete mode 100644 packages/ra-core/src/core/useWrappedSource.ts create mode 100644 packages/ra-core/src/i18n/useTranslateLabel.spec.tsx diff --git a/packages/ra-core/src/core/SourceContext.tsx b/packages/ra-core/src/core/SourceContext.tsx index f514fb9ba2c..231d322f6cb 100644 --- a/packages/ra-core/src/core/SourceContext.tsx +++ b/packages/ra-core/src/core/SourceContext.tsx @@ -1,14 +1,27 @@ import { createContext, useContext } from 'react'; -export type SourceContextValue = (source: string) => string; +export type SourceContextValue = { + /* + * Returns the source for a field or input, modified according to the context. + */ + getSource: (source: string) => string; + /* + * Returns the label for a field or input, modified according to the context. Returns a translation key. + */ + getLabel: (source: string) => string; +}; /** * Context that provides a function that accept a source and return a modified source (prefixed, suffixed, etc.) for fields and inputs. * * @example - * const CoordinatesInput = props => { + * const sourceContext = { + * getSource: source => `coordinates.${source}`, + * getLabel: source => `resources.posts.fields.${source}`, + * } + * const CoordinatesInput = () => { * return ( - * `coordinates.${source}`}> + * * * * diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts index f92012eff85..3783ee90886 100644 --- a/packages/ra-core/src/core/index.ts +++ b/packages/ra-core/src/core/index.ts @@ -14,4 +14,3 @@ export * from './useResourceDefinition'; export * from './useResourceDefinitions'; export * from './useGetRecordRepresentation'; export * from './SourceContext'; -export * from './useWrappedSource'; diff --git a/packages/ra-core/src/core/useWrappedSource.ts b/packages/ra-core/src/core/useWrappedSource.ts deleted file mode 100644 index a1808d9e0c5..00000000000 --- a/packages/ra-core/src/core/useWrappedSource.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useContext } from 'react'; -import { SourceContext } from './SourceContext'; - -/** - * Hook to get a source that may be prefixed, suffixed, etc. by a parent component. You don't need to call this hook if you use the `useInput` hook. - * @param source The original field or input source - * @returns The modified source if the calling component is inside a `SourceContext`, the original source otherwise. - * - * @example - * const MyInput = ({ source, ...props }) => { - * const finalSource = useWrappedSource(source); - * return ; - * }; - */ -export const useWrappedSource = (source: string) => { - const context = useContext(SourceContext); - return context ? context(source) : source; -}; diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index eeba567c459..c72f5b5dac7 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import { useFormContext, FieldValues } from 'react-hook-form'; import get from 'lodash/get'; import { useFormValues } from './useFormValues'; -import { useWrappedSource } from '../core'; +import { useSourceContext } from '../core'; /** * Get the current (edited) value of the record from the form and pass it @@ -68,8 +68,11 @@ export const FormDataConsumerView = < const { children, form, formData, source, ...rest } = props; let ret; - const finalSource = useWrappedSource(''); - const matches = ArraySourceRegex.exec(finalSource); + const sourceContext = useSourceContext(); + // Passes an empty string here as we don't have the children sources and we just want to know if we are in an iterator + const matches = ArraySourceRegex.exec( + sourceContext?.getSource('') ?? source + ); // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) if (matches) { diff --git a/packages/ra-core/src/form/useApplyInputDefaultValues.ts b/packages/ra-core/src/form/useApplyInputDefaultValues.ts index 336f9e44cd2..c49eb088bb4 100644 --- a/packages/ra-core/src/form/useApplyInputDefaultValues.ts +++ b/packages/ra-core/src/form/useApplyInputDefaultValues.ts @@ -7,7 +7,7 @@ import { import get from 'lodash/get'; import { useRecordContext } from '../controller'; import { InputProps } from './useInput'; -import { useWrappedSource } from '../core'; +import { useSourceContext } from '../core'; interface StandardInput { inputProps: Partial & { source: string }; @@ -33,7 +33,8 @@ export const useApplyInputDefaultValues = ({ fieldArrayInputControl, }: Props) => { const { defaultValue, source } = inputProps; - const finalSource = useWrappedSource(source); + const sourceContext = useSourceContext(); + const finalSource = sourceContext?.getSource(source) ?? source; const record = useRecordContext(inputProps); const { @@ -43,7 +44,7 @@ export const useApplyInputDefaultValues = ({ formState, reset, } = useFormContext(); - const recordValue = get(record, source); + const recordValue = get(record, finalSource); const formValue = get(getValues(), finalSource); const { isDirty } = getFieldState(finalSource, formState); diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index f15413e0d22..1541b982e90 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -16,7 +16,7 @@ import { useFormGroupContext } from './useFormGroupContext'; import { useFormGroups } from './useFormGroups'; import { useApplyInputDefaultValues } from './useApplyInputDefaultValues'; import { useEvent } from '../util'; -import { useWrappedSource } from '../core'; +import { useSourceContext } from '../core'; // replace null or undefined values by empty string to avoid controlled/uncontrolled input warning const defaultFormat = (value: any) => (value == null ? '' : value); @@ -39,7 +39,8 @@ export const useInput = ( validate, ...options } = props; - const finalSource = useWrappedSource(source); + const sourceContext = useSourceContext(); + const finalSource = sourceContext?.getSource(source) ?? source; const finalName = name || finalSource; const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); diff --git a/packages/ra-core/src/i18n/TestTranslationProvider.tsx b/packages/ra-core/src/i18n/TestTranslationProvider.tsx index aad86867de0..e89cc533a23 100644 --- a/packages/ra-core/src/i18n/TestTranslationProvider.tsx +++ b/packages/ra-core/src/i18n/TestTranslationProvider.tsx @@ -13,7 +13,6 @@ export const TestTranslationProvider = ({ translate: messages ? (key: string, options?: any) => { const message = lodashGet(messages, key); - console.log({ key, options, message }); return message ? typeof message === 'function' ? message(options) diff --git a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx new file mode 100644 index 00000000000..11c9c39f93e --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useTranslateLabel } from './useTranslateLabel'; +import { TestTranslationProvider } from './TestTranslationProvider'; +import { SourceContextProvider } from '..'; + +describe('useTranslateLabel', () => { + const TranslateLabel = ({ + source, + label, + resource, + }: { + source?: string; + label?: string | false | React.ReactElement; + resource?: string; + }) => { + const translateLabel = useTranslateLabel(); + return ( + <> + {translateLabel({ + label, + source, + resource, + })} + + ); + }; + + it('should return null when label is false', () => { + render( + + + + ); + expect(screen.queryByText(/title/)).toBeNull(); + }); + + it('should return null when label is empty', () => { + render( + + + + ); + expect(screen.queryByText(/title/)).toBeNull(); + }); + + it('should return the label element when provided', () => { + render( + + My title} + source="title" + resource="posts" + /> + + ); + screen.getByText('My title'); + }); + + it('should return the label text when provided', () => { + render( + + + + ); + screen.getByText('My title'); + }); + + it('should return the translated label text when provided', () => { + render( + + + + ); + screen.getByText('My title'); + }); + + it('should return the inferred label from source and resource when no label is provided', () => { + render( + + + + ); + screen.getByText('Title'); + }); + + it('should return the translated inferred label from source and resource when no label is provided', () => { + render( + + + + ); + screen.getByText('My Title'); + }); + + it('should return the label from SourceContext when no label is provided but a SourceContext is present', () => { + render( + + source, + getLabel: source => `test.${source}`, + }} + > + + + + ); + screen.getByText('Title'); + }); + + it('should return the translated label from SourceContext when no label is provided but a SourceContext is present', () => { + render( + + source, + getLabel: source => `test.${source}`, + }} + > + + + + ); + screen.getByText('Label for title'); + }); +}); diff --git a/packages/ra-core/src/i18n/useTranslateLabel.ts b/packages/ra-core/src/i18n/useTranslateLabel.ts index 47f28258829..1055677aa58 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.ts +++ b/packages/ra-core/src/i18n/useTranslateLabel.ts @@ -2,12 +2,13 @@ import { useCallback, ReactElement } from 'react'; import { useTranslate } from './useTranslate'; import { useLabelPrefix, getFieldLabelTranslationArgs } from '../util'; -import { useResourceContext } from '../core'; +import { useResourceContext, useSourceContext } from '../core'; export const useTranslateLabel = () => { const translate = useTranslate(); const prefix = useLabelPrefix(); const resourceFromContext = useResourceContext(); + const sourceContext = useSourceContext(); return useCallback( ({ @@ -19,6 +20,8 @@ export const useTranslateLabel = () => { label?: string | false | ReactElement; resource?: string; }) => { + const finalSource = sourceContext?.getSource(source) ?? source; + if (label === false || label === '') { return null; } @@ -27,16 +30,34 @@ export const useTranslateLabel = () => { return label; } + if (label && typeof label === 'string') { + return translate(label, { _: label }); + } + + const sourceContextLabel = sourceContext?.getLabel(source); + + if (sourceContextLabel) { + return translate( + sourceContextLabel, + // Here we want the default inferred label if the translation is missing + getFieldLabelTranslationArgs({ + prefix, + resource, + resourceFromContext, + source: finalSource, + })[1] + ); + } + return translate( ...getFieldLabelTranslationArgs({ - label: label as string, prefix, resource, resourceFromContext, - source, + source: finalSource, }) ); }, - [prefix, resourceFromContext, translate] + [prefix, resourceFromContext, translate, sourceContext] ); }; diff --git a/packages/ra-core/src/util/FieldTitle.tsx b/packages/ra-core/src/util/FieldTitle.tsx index 01c33275210..e5cf2df744c 100644 --- a/packages/ra-core/src/util/FieldTitle.tsx +++ b/packages/ra-core/src/util/FieldTitle.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { ReactElement, memo } from 'react'; import { useTranslateLabel } from '../i18n'; -import { useWrappedSource } from '../core'; export interface FieldTitleProps { isRequired?: boolean; @@ -14,7 +13,6 @@ export interface FieldTitleProps { export const FieldTitle = (props: FieldTitleProps) => { const { source, label, resource, isRequired } = props; const translateLabel = useTranslateLabel(); - const finalSource = useWrappedSource(source); if (label === true) { throw new Error( @@ -35,7 +33,7 @@ export const FieldTitle = (props: FieldTitleProps) => { {translateLabel({ label, resource, - source: finalSource, + source, })} {isRequired && } diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts index f693abe149f..0fc1daaefc8 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts @@ -5,16 +5,6 @@ describe('getFieldLabelTranslationArgs', () => { it('should return empty span by default', () => expect(getFieldLabelTranslationArgs()).toEqual([''])); - it('should return the label when given', () => { - expect( - getFieldLabelTranslationArgs({ - label: 'foo', - resource: 'posts', - source: 'title', - }) - ).toEqual(['foo', { _: 'foo' }]); - }); - it('should return the source and resource as translate key', () => { expect( getFieldLabelTranslationArgs({ diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts index bdcbd6a6b8f..9411fb839da 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts @@ -12,7 +12,7 @@ type TranslationArguments = [string, any?]; /** * Returns an array of arguments to use with the translate function for the label of a field. - * The label will be the one specified by the label prop or one computed from the resource and source props. + * The label will be computed from the resource and source props. * * Usage: * @@ -24,9 +24,7 @@ type TranslationArguments = [string, any?]; export default (options?: Args): TranslationArguments => { if (!options) return ['']; - const { label, prefix, resource, resourceFromContext, source } = options; - - if (typeof label !== 'undefined') return [label, { _: label }]; + const { prefix, resource, resourceFromContext, source } = options; if (typeof source === 'undefined') return ['']; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index cf2a2d757f8..b50a920ff39 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -10,7 +10,7 @@ import { useGetValidationErrorMessage, useFormGroupContext, useFormGroups, - useWrappedSource, + useSourceContext, } from 'ra-core'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { @@ -91,7 +91,8 @@ export const ArrayInput = (props: ArrayInputProps) => { const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); - const finalSource = useWrappedSource(source); + const sourceContext = useSourceContext(); + const finalSource = sourceContext?.getSource(source) ?? source; const sanitizedValidate = Array.isArray(validate) ? composeSyncValidators(validate) diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index a71b9804b8a..29a27bc4ecb 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -9,7 +9,7 @@ import { } from 'react'; import { Typography } from '@mui/material'; import clsx from 'clsx'; -import { RaRecord, SourceContextProvider } from 'ra-core'; +import { RaRecord, SourceContextProvider, useResourceContext } from 'ra-core'; import { SimpleFormIteratorClasses } from './useSimpleFormIteratorStyles'; import { useSimpleFormIterator } from './useSimpleFormIterator'; @@ -35,7 +35,7 @@ export const SimpleFormIteratorItem = React.forwardRef( reOrderButtons, source, } = props; - + const resource = useResourceContext(props); const { total, reOrder, remove } = useSimpleFormIterator(); // Returns a boolean to indicate whether to disable the remove button for certain fields. // If disableRemove is a function, then call the function with the current record to @@ -75,8 +75,18 @@ export const SimpleFormIteratorItem = React.forwardRef( : getItemLabel; const sourceContext = useMemo( - () => (source: string) => (source ? `${member}.${source}` : member), - [member] + () => ({ + getSource: (source: string) => + source ? `${member}.${source}` : member, + getLabel: (source: string) => { + // remove digits, e.g. 'book.authors.2.categories.3.identifier.name' => 'book.authors.categories.identifier.name' + return `resources.${resource}.fields.${member.replace( + /\.\d+/g, + '' + )}.${source}`; + }, + }), + [member, resource] ); return ( diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx index ea314832f05..3628f58292a 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx @@ -126,17 +126,17 @@ describe('', () => { ); fireEvent.change( - screen.getByLabelText('resources.undefined.fields.name.en'), + screen.getAllByLabelText('resources.undefined.fields.name')[0], { target: { value: 'english value' }, } ); fireEvent.click(screen.getByText('ra.locales.fr')); fireEvent.focus( - screen.getByLabelText('resources.undefined.fields.name.fr') + screen.getAllByLabelText('resources.undefined.fields.name')[1] ); fireEvent.blur( - screen.getByLabelText('resources.undefined.fields.name.fr') + screen.getAllByLabelText('resources.undefined.fields.name')[1] ); await waitFor(() => { expect(screen.queryByText('error')).not.toBeNull(); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index 6ffdad1e74b..bf7f030d9f9 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -18,10 +18,13 @@ export const TranslatableInputsTabContent = ( props: TranslatableInputsTabContentProps ): ReactElement => { const { children, groupKey = '', locale, ...other } = props; - const { selectedLocale, getSource } = useTranslatableContext(); + const { selectedLocale, getLabel, getSource } = useTranslatableContext(); const sourceContext = useMemo( - () => (source: string) => getSource(source, locale), - [getSource, locale] + () => ({ + getSource: (source: string) => getSource(source, locale), + getLabel, + }), + [getLabel, getSource, locale] ); return ( From 35c6d9bb47f78d9792c35a560c9e20f7164628a6 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:31:00 +0100 Subject: [PATCH 16/28] Refactor to centralize label logic --- .../ra-core/src/i18n/useTranslateLabel.ts | 21 ++----------------- .../src/util/getFieldLabelTranslationArgs.ts | 16 +++++++++++++- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/ra-core/src/i18n/useTranslateLabel.ts b/packages/ra-core/src/i18n/useTranslateLabel.ts index 1055677aa58..e8d3e6104da 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.ts +++ b/packages/ra-core/src/i18n/useTranslateLabel.ts @@ -30,27 +30,10 @@ export const useTranslateLabel = () => { return label; } - if (label && typeof label === 'string') { - return translate(label, { _: label }); - } - - const sourceContextLabel = sourceContext?.getLabel(source); - - if (sourceContextLabel) { - return translate( - sourceContextLabel, - // Here we want the default inferred label if the translation is missing - getFieldLabelTranslationArgs({ - prefix, - resource, - resourceFromContext, - source: finalSource, - })[1] - ); - } - return translate( ...getFieldLabelTranslationArgs({ + label: label as string, + labelFromSourceContext: sourceContext?.getLabel(source), prefix, resource, resourceFromContext, diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts index 9411fb839da..7cf1c634b48 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts @@ -2,6 +2,7 @@ import inflection from 'inflection'; interface Args { label?: string; + labelFromSourceContext?: string; prefix?: string; resource?: string; resourceFromContext?: string; @@ -24,7 +25,16 @@ type TranslationArguments = [string, any?]; export default (options?: Args): TranslationArguments => { if (!options) return ['']; - const { prefix, resource, resourceFromContext, source } = options; + const { + label, + labelFromSourceContext, + prefix, + resource, + resourceFromContext, + source, + } = options; + + if (typeof label !== 'undefined') return [label, { _: label }]; if (typeof source === 'undefined') return ['']; @@ -35,6 +45,10 @@ export default (options?: Args): TranslationArguments => { ['underscore', 'humanize'] ); + if (labelFromSourceContext) { + return [labelFromSourceContext, { _: defaultLabel }]; + } + if (resource) { return [ `resources.${resource}.fields.${sourceWithoutDigits}`, From 59bc45fbef985ad7c4a7e59bc67b305c8e5d8763 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:48:32 +0100 Subject: [PATCH 17/28] Restore removed test --- .../src/util/getFieldLabelTranslationArgs.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts index 0fc1daaefc8..f693abe149f 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts @@ -5,6 +5,16 @@ describe('getFieldLabelTranslationArgs', () => { it('should return empty span by default', () => expect(getFieldLabelTranslationArgs()).toEqual([''])); + it('should return the label when given', () => { + expect( + getFieldLabelTranslationArgs({ + label: 'foo', + resource: 'posts', + source: 'title', + }) + ).toEqual(['foo', { _: 'foo' }]); + }); + it('should return the source and resource as translate key', () => { expect( getFieldLabelTranslationArgs({ From d9c9fb8de343c79675f4075f110ccf5f6c5a5a6d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:48:46 +0100 Subject: [PATCH 18/28] Apply review suggestion --- packages/ra-core/src/i18n/useTranslateLabel.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx index 11c9c39f93e..d166e0367ae 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx +++ b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx @@ -115,7 +115,7 @@ describe('useTranslateLabel', () => { screen.getByText('My Title'); }); - it('should return the label from SourceContext when no label is provided but a SourceContext is present', () => { + it('should return the inferred label from SourceContext when no label is provided but a SourceContext is present', () => { render( Date: Mon, 8 Jan 2024 13:28:21 +0100 Subject: [PATCH 19/28] Apply suggestions from code review Co-authored-by: Francois Zaninotto --- docs/Inputs.md | 2 +- docs/SimpleFormIterator.md | 2 +- docs/Upgrade.md | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index 9a7a43c4e4e..15848b3c7a6 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -567,7 +567,7 @@ const PostEdit = () => ( ); ``` -**Tip:** TypeScript users will notice that `scopedFormData` is typed as optional parameters. This is because the `` component can be used outside of an `` and in that case, these parameters will be `undefined`. If you are inside an ``, you can safely assume that these parameters will be defined. +**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `` component can be used outside of an `` and in that case, this parameter will be `undefined`. If you are inside an ``, you can safely assume that this parameter will be defined. ## Hiding Inputs Based On Other Inputs diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 8c6e7761f90..769ab2d9e4b 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -147,7 +147,7 @@ const PostEdit = () => ( ); ``` -**Tip:** TypeScript users will notice that `scopedFormData` is typed as optional parameters. This is because the `` component can be used outside of a `` and in that case, these parameters will be `undefined`. If you are inside a ``, you can safely assume that these parameters will be defined. +**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `` component can be used outside of an `` and in that case, this parameter will be `undefined`. If you are inside an ``, you can safely assume that this parameter will be defined. **Note**: `` only accepts `Input` components as children. If you want to use some `Fields` instead, you have to use a ``, as follows: diff --git a/docs/Upgrade.md b/docs/Upgrade.md index a129ced2c86..7091557140c 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -219,9 +219,9 @@ We've changed the implementation of ``, the companion child ``` -## `` no longer pass a `getSource` function +## `` no longer passes a `getSource` function -As we introduced the `SourceContext`, we don't need the `getSource` function anymore when using the `` inside an ``: +When using `` inside an ``, the child function no longer receives a `getSource` callback. We've made all Input components able to work seamlessly inside an ``, so it's no longer necessary to transform their source with `getSource`: ```diff import { Edit, SimpleForm, TextInput, ArrayInput, SelectInput, FormDataConsumer } from 'react-admin'; @@ -236,13 +236,13 @@ const PostEdit = () => ( {({ formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput -- getSource, // A function to get the valid source inside an ArrayInput +- getSource, ...rest }) => scopedFormData && getSource && scopedFormData.name ? ( From e602813f74e8a1a2df27fc0f37e370a5e3d0a070 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:38:56 +0100 Subject: [PATCH 20/28] Reintroduce useWrappedSource --- packages/ra-core/src/core/index.ts | 3 ++- packages/ra-core/src/core/useWrappedSource.ts | 16 ++++++++++++++++ packages/ra-core/src/form/FormDataConsumer.tsx | 8 +++----- .../src/form/useApplyInputDefaultValues.ts | 5 ++--- packages/ra-core/src/form/useInput.ts | 5 ++--- .../src/input/ArrayInput/ArrayInput.tsx | 5 ++--- 6 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 packages/ra-core/src/core/useWrappedSource.ts diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts index 3783ee90886..4409feaf86f 100644 --- a/packages/ra-core/src/core/index.ts +++ b/packages/ra-core/src/core/index.ts @@ -7,10 +7,11 @@ export * from './Resource'; export * from './ResourceContext'; export * from './ResourceContextProvider'; export * from './ResourceDefinitionContext'; +export * from './SourceContext'; export * from './useGetResourceLabel'; export * from './useResourceDefinitionContext'; export * from './useResourceContext'; export * from './useResourceDefinition'; export * from './useResourceDefinitions'; export * from './useGetRecordRepresentation'; -export * from './SourceContext'; +export * from './useWrappedSource'; diff --git a/packages/ra-core/src/core/useWrappedSource.ts b/packages/ra-core/src/core/useWrappedSource.ts new file mode 100644 index 00000000000..33c3e5fe5cb --- /dev/null +++ b/packages/ra-core/src/core/useWrappedSource.ts @@ -0,0 +1,16 @@ +import { useSourceContext } from './SourceContext'; + +/** + * Get the source prop for a field or input by checking if a source context is available. + * @param {string} source The original source prop + * @returns {string} The source prop, either the original one or the one modified by the SourceContext. + * @example + * const MyInput = ({ source, ...props }) => { + * const finalSource = useWrappedSource(source); + * return ; + * }; + */ +export const useWrappedSource = (source: string) => { + const sourceContext = useSourceContext(); + return sourceContext?.getSource(source) ?? source; +}; diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index c72f5b5dac7..fd7e1c75ba5 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import { useFormContext, FieldValues } from 'react-hook-form'; import get from 'lodash/get'; import { useFormValues } from './useFormValues'; -import { useSourceContext } from '../core'; +import { useWrappedSource } from '../core'; /** * Get the current (edited) value of the record from the form and pass it @@ -68,11 +68,9 @@ export const FormDataConsumerView = < const { children, form, formData, source, ...rest } = props; let ret; - const sourceContext = useSourceContext(); + const finalSource = useWrappedSource(source); // Passes an empty string here as we don't have the children sources and we just want to know if we are in an iterator - const matches = ArraySourceRegex.exec( - sourceContext?.getSource('') ?? source - ); + const matches = ArraySourceRegex.exec(finalSource); // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) if (matches) { diff --git a/packages/ra-core/src/form/useApplyInputDefaultValues.ts b/packages/ra-core/src/form/useApplyInputDefaultValues.ts index c49eb088bb4..267bc0aa12b 100644 --- a/packages/ra-core/src/form/useApplyInputDefaultValues.ts +++ b/packages/ra-core/src/form/useApplyInputDefaultValues.ts @@ -7,7 +7,7 @@ import { import get from 'lodash/get'; import { useRecordContext } from '../controller'; import { InputProps } from './useInput'; -import { useSourceContext } from '../core'; +import { useWrappedSource } from '../core'; interface StandardInput { inputProps: Partial & { source: string }; @@ -33,8 +33,7 @@ export const useApplyInputDefaultValues = ({ fieldArrayInputControl, }: Props) => { const { defaultValue, source } = inputProps; - const sourceContext = useSourceContext(); - const finalSource = sourceContext?.getSource(source) ?? source; + const finalSource = useWrappedSource(source); const record = useRecordContext(inputProps); const { diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index 1541b982e90..f15413e0d22 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -16,7 +16,7 @@ import { useFormGroupContext } from './useFormGroupContext'; import { useFormGroups } from './useFormGroups'; import { useApplyInputDefaultValues } from './useApplyInputDefaultValues'; import { useEvent } from '../util'; -import { useSourceContext } from '../core'; +import { useWrappedSource } from '../core'; // replace null or undefined values by empty string to avoid controlled/uncontrolled input warning const defaultFormat = (value: any) => (value == null ? '' : value); @@ -39,8 +39,7 @@ export const useInput = ( validate, ...options } = props; - const sourceContext = useSourceContext(); - const finalSource = sourceContext?.getSource(source) ?? source; + const finalSource = useWrappedSource(source); const finalName = name || finalSource; const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx index b50a920ff39..cf2a2d757f8 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx @@ -10,7 +10,7 @@ import { useGetValidationErrorMessage, useFormGroupContext, useFormGroups, - useSourceContext, + useWrappedSource, } from 'ra-core'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { @@ -91,8 +91,7 @@ export const ArrayInput = (props: ArrayInputProps) => { const formGroupName = useFormGroupContext(); const formGroups = useFormGroups(); - const sourceContext = useSourceContext(); - const finalSource = sourceContext?.getSource(source) ?? source; + const finalSource = useWrappedSource(source); const sanitizedValidate = Array.isArray(validate) ? composeSyncValidators(validate) From b1f8ce13bd1fe19fa44624f13425dc9fdecbb3df Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:51:43 +0100 Subject: [PATCH 21/28] Compute TranslatableInputs label translationKey in place --- packages/ra-core/src/i18n/useTranslatable.ts | 2 ++ .../src/input/TranslatableInputsTabContent.tsx | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/src/i18n/useTranslatable.ts b/packages/ra-core/src/i18n/useTranslatable.ts index ce7c5acad7c..876ab0c3585 100644 --- a/packages/ra-core/src/i18n/useTranslatable.ts +++ b/packages/ra-core/src/i18n/useTranslatable.ts @@ -30,8 +30,10 @@ export const useTranslatable = ( const context = useMemo( () => ({ + // TODO: remove once fields use SourceContext getSource: (source: string, locale: string = selectedLocale) => `${source}.${locale}`, + // TODO: remove once fields use SourceContext getLabel: (source: string, label?: string) => translateLabel({ source, resource, label }) as string, locales, diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index bf7f030d9f9..3d3b1229233 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -7,6 +7,7 @@ import { FormGroupContextProvider, RaRecord, SourceContextProvider, + useResourceContext, useTranslatableContext, } from 'ra-core'; @@ -18,13 +19,18 @@ export const TranslatableInputsTabContent = ( props: TranslatableInputsTabContentProps ): ReactElement => { const { children, groupKey = '', locale, ...other } = props; - const { selectedLocale, getLabel, getSource } = useTranslatableContext(); + const resource = useResourceContext(props); + const { selectedLocale } = useTranslatableContext(); const sourceContext = useMemo( () => ({ - getSource: (source: string) => getSource(source, locale), - getLabel, + getSource: (source: string) => `${source}.${locale}`, + getLabel: (source: string) => + `resources.${resource}.fields.${source.replace( + `.${locale}`, + '' + )}`, }), - [getLabel, getSource, locale] + [locale, resource] ); return ( From 8c1d5b7936dfa3fc710bc8b4babcc1c1a0adbe0f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:14:50 +0100 Subject: [PATCH 22/28] Rename variables --- .../ra-core/src/i18n/useTranslateLabel.ts | 2 +- .../src/util/getFieldLabelTranslationArgs.ts | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/ra-core/src/i18n/useTranslateLabel.ts b/packages/ra-core/src/i18n/useTranslateLabel.ts index e8d3e6104da..7e3875d6d08 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.ts +++ b/packages/ra-core/src/i18n/useTranslateLabel.ts @@ -33,7 +33,7 @@ export const useTranslateLabel = () => { return translate( ...getFieldLabelTranslationArgs({ label: label as string, - labelFromSourceContext: sourceContext?.getLabel(source), + defaultLabel: sourceContext?.getLabel(source), prefix, resource, resourceFromContext, diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts index 7cf1c634b48..424eae6866e 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts @@ -2,7 +2,7 @@ import inflection from 'inflection'; interface Args { label?: string; - labelFromSourceContext?: string; + defaultLabel?: string; prefix?: string; resource?: string; resourceFromContext?: string; @@ -27,7 +27,7 @@ export default (options?: Args): TranslationArguments => { const { label, - labelFromSourceContext, + defaultLabel, prefix, resource, resourceFromContext, @@ -40,29 +40,32 @@ export default (options?: Args): TranslationArguments => { const { sourceWithoutDigits, sourceSuffix } = getSourceParts(source); - const defaultLabel = inflection.transform( + const defaultLabelTranslation = inflection.transform( sourceSuffix.replace(/\./g, ' '), ['underscore', 'humanize'] ); - if (labelFromSourceContext) { - return [labelFromSourceContext, { _: defaultLabel }]; + if (defaultLabel) { + return [defaultLabel, { _: defaultLabelTranslation }]; } if (resource) { return [ `resources.${resource}.fields.${sourceWithoutDigits}`, - { _: defaultLabel }, + { _: defaultLabelTranslation }, ]; } if (prefix) { - return [`${prefix}.${sourceWithoutDigits}`, { _: defaultLabel }]; + return [ + `${prefix}.${sourceWithoutDigits}`, + { _: defaultLabelTranslation }, + ]; } return [ `resources.${resourceFromContext}.fields.${sourceWithoutDigits}`, - { _: defaultLabel }, + { _: defaultLabelTranslation }, ]; }; From cc0e4611acb62668632616f9b1e024e4e94a09a1 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:51:32 +0100 Subject: [PATCH 23/28] Replace LabelPrefix with SourceContext --- packages/ra-core/src/form/Form.tsx | 11 +++++--- .../src/i18n/useTranslateLabel.spec.tsx | 24 ++++++++++++++++- .../ra-core/src/i18n/useTranslateLabel.ts | 6 ++--- .../ra-core/src/util/LabelPrefixContext.ts | 3 --- .../src/util/LabelPrefixContextProvider.tsx | 18 ------------- .../util/getFieldLabelTranslationArgs.spec.ts | 10 +++---- .../src/util/getFieldLabelTranslationArgs.ts | 26 +++++++++---------- packages/ra-core/src/util/index.ts | 6 +---- packages/ra-core/src/util/useLabelPrefix.ts | 4 --- .../ArrayInput/SimpleFormIteratorItem.tsx | 16 +++++++++--- .../input/TranslatableInputsTabContent.tsx | 16 +++++++----- .../src/list/filter/FilterForm.tsx | 16 +++++++++--- 12 files changed, 86 insertions(+), 70 deletions(-) delete mode 100644 packages/ra-core/src/util/LabelPrefixContext.ts delete mode 100644 packages/ra-core/src/util/LabelPrefixContextProvider.tsx delete mode 100644 packages/ra-core/src/util/useLabelPrefix.ts diff --git a/packages/ra-core/src/form/Form.tsx b/packages/ra-core/src/form/Form.tsx index ebd01f28910..5d440bf6d4a 100644 --- a/packages/ra-core/src/form/Form.tsx +++ b/packages/ra-core/src/form/Form.tsx @@ -14,8 +14,7 @@ import { OptionalRecordContextProvider, SaveHandler, } from '../controller'; -import { useResourceContext } from '../core'; -import { LabelPrefixContextProvider } from '../util'; +import { SourceContextProvider, SourceContextValue, useResourceContext } from '../core'; import { ValidateForm } from './getSimpleValidationResolver'; import { useAugmentedForm } from './useAugmentedForm'; @@ -53,10 +52,14 @@ export const Form = (props: FormProps) => { const record = useRecordContext(props); const resource = useResourceContext(props); const { form, formHandleSubmit } = useAugmentedForm(props); + const sourceContext = React.useMemo(() => ({ + getSource: (source: string) => source, + getLabel: (source: string) => `resources.${resource}.fields.${source}`, + }), [resource]); return ( - +
    (props: FormProps) => {
    -
    +
    ); }; diff --git a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx index d166e0367ae..a863f289493 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx +++ b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx @@ -146,10 +146,32 @@ describe('useTranslateLabel', () => { getLabel: source => `test.${source}`, }} > - +
    ); screen.getByText('Label for title'); }); + + it('should return the inferred label when a resource prop is provided even when a SourceContext is present', () => { + render( + + source, + getLabel: source => `test.${source}`, + }} + > + + + + ); + screen.getByText('Title'); + }); }); diff --git a/packages/ra-core/src/i18n/useTranslateLabel.ts b/packages/ra-core/src/i18n/useTranslateLabel.ts index 7e3875d6d08..1bebd4cd78a 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.ts +++ b/packages/ra-core/src/i18n/useTranslateLabel.ts @@ -1,12 +1,11 @@ import { useCallback, ReactElement } from 'react'; import { useTranslate } from './useTranslate'; -import { useLabelPrefix, getFieldLabelTranslationArgs } from '../util'; +import { getFieldLabelTranslationArgs } from '../util'; import { useResourceContext, useSourceContext } from '../core'; export const useTranslateLabel = () => { const translate = useTranslate(); - const prefix = useLabelPrefix(); const resourceFromContext = useResourceContext(); const sourceContext = useSourceContext(); @@ -34,13 +33,12 @@ export const useTranslateLabel = () => { ...getFieldLabelTranslationArgs({ label: label as string, defaultLabel: sourceContext?.getLabel(source), - prefix, resource, resourceFromContext, source: finalSource, }) ); }, - [prefix, resourceFromContext, translate, sourceContext] + [resourceFromContext, translate, sourceContext] ); }; diff --git a/packages/ra-core/src/util/LabelPrefixContext.ts b/packages/ra-core/src/util/LabelPrefixContext.ts deleted file mode 100644 index 5ade1aa9910..00000000000 --- a/packages/ra-core/src/util/LabelPrefixContext.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const LabelPrefixContext = createContext(''); diff --git a/packages/ra-core/src/util/LabelPrefixContextProvider.tsx b/packages/ra-core/src/util/LabelPrefixContextProvider.tsx deleted file mode 100644 index b5d0efa6cf0..00000000000 --- a/packages/ra-core/src/util/LabelPrefixContextProvider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { LabelPrefixContext } from './LabelPrefixContext'; -import { useLabelPrefix } from './useLabelPrefix'; - -export const LabelPrefixContextProvider = ({ - prefix, - concatenate = true, - children, -}) => { - const oldPrefix = useLabelPrefix(); - const newPrefix = - oldPrefix && concatenate ? `${oldPrefix}.${prefix}` : prefix; - return ( - - {children} - - ); -}; diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts index f693abe149f..8bf08f8aef7 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts @@ -80,17 +80,17 @@ describe('getFieldLabelTranslationArgs', () => { getFieldLabelTranslationArgs({ resource: 'posts', resourceFromContext: 'users', - prefix: 'resources.users.fields', + defaultLabel: 'resources.users.fields.name', source: 'referenceOne.users@@name', }) ).toEqual(['resources.users.fields.name', { _: 'Name' }]); }); - it('should prefer the resource over the prefix', () => { + it('should prefer the resource over the defaultLabel', () => { expect( getFieldLabelTranslationArgs({ resource: 'books', - prefix: 'resources.posts.fields', + defaultLabel: 'resources.posts.fields.title', source: 'title', }) ).toEqual([`resources.books.fields.title`, { _: 'Title' }]); @@ -106,10 +106,10 @@ describe('getFieldLabelTranslationArgs', () => { ).toEqual([`resources.posts.fields.title`, { _: 'Title' }]); }); - it('should prefer the prefix over the resourceFromContext', () => { + it('should prefer the defaultLabel over the resourceFromContext', () => { expect( getFieldLabelTranslationArgs({ - prefix: 'resources.posts.fields', + defaultLabel: 'resources.posts.fields.title', resourceFromContext: 'books', source: 'title', }) diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts index 424eae6866e..e929def61df 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts @@ -3,7 +3,6 @@ import inflection from 'inflection'; interface Args { label?: string; defaultLabel?: string; - prefix?: string; resource?: string; resourceFromContext?: string; source?: string; @@ -22,13 +21,14 @@ type TranslationArguments = [string, any?]; * * @see useTranslateLabel for a ready-to-use hook */ -export default (options?: Args): TranslationArguments => { +export const getFieldLabelTranslationArgs = ( + options?: Args +): TranslationArguments => { if (!options) return ['']; const { label, defaultLabel, - prefix, resource, resourceFromContext, source, @@ -45,30 +45,28 @@ export default (options?: Args): TranslationArguments => { ['underscore', 'humanize'] ); - if (defaultLabel) { - return [defaultLabel, { _: defaultLabelTranslation }]; - } - if (resource) { return [ - `resources.${resource}.fields.${sourceWithoutDigits}`, + getResourceFieldLabelKey(resource, sourceWithoutDigits), { _: defaultLabelTranslation }, ]; } - if (prefix) { - return [ - `${prefix}.${sourceWithoutDigits}`, - { _: defaultLabelTranslation }, - ]; + if (defaultLabel) { + return [defaultLabel, { _: defaultLabelTranslation }]; } return [ - `resources.${resourceFromContext}.fields.${sourceWithoutDigits}`, + getResourceFieldLabelKey(resourceFromContext, sourceWithoutDigits), { _: defaultLabelTranslation }, ]; }; +export default getFieldLabelTranslationArgs; + +export const getResourceFieldLabelKey = (resource: string, source: string) => + `resources.${resource}.fields.${source}`; + /** * Uses the source string to guess a translation message and a default label. * diff --git a/packages/ra-core/src/util/index.ts b/packages/ra-core/src/util/index.ts index c3aa327aeec..d02a278ad07 100644 --- a/packages/ra-core/src/util/index.ts +++ b/packages/ra-core/src/util/index.ts @@ -1,6 +1,5 @@ import escapePath from './escapePath'; import FieldTitle, { FieldTitleProps } from './FieldTitle'; -import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs'; import ComponentPropType from './ComponentPropType'; import removeEmpty from './removeEmpty'; import removeKey from './removeKey'; @@ -8,13 +7,13 @@ import Ready from './Ready'; import warning from './warning'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; import { getMutationMode } from './getMutationMode'; +export * from './getFieldLabelTranslationArgs'; export * from './mergeRefs'; export * from './useEvent'; export { escapePath, FieldTitle, - getFieldLabelTranslationArgs, ComponentPropType, Ready, removeEmpty, @@ -28,7 +27,4 @@ export type { FieldTitleProps }; export * from './asyncDebounce'; export * from './hooks'; export * from './shallowEqual'; -export * from './LabelPrefixContext'; -export * from './LabelPrefixContextProvider'; -export * from './useLabelPrefix'; export * from './useCheckForApplicationUpdate'; diff --git a/packages/ra-core/src/util/useLabelPrefix.ts b/packages/ra-core/src/util/useLabelPrefix.ts deleted file mode 100644 index 24dda069ddf..00000000000 --- a/packages/ra-core/src/util/useLabelPrefix.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { useContext } from 'react'; -import { LabelPrefixContext } from './LabelPrefixContext'; - -export const useLabelPrefix = () => useContext(LabelPrefixContext); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index 29a27bc4ecb..18fc6aa36bf 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -9,7 +9,13 @@ import { } from 'react'; import { Typography } from '@mui/material'; import clsx from 'clsx'; -import { RaRecord, SourceContextProvider, useResourceContext } from 'ra-core'; +import { + getResourceFieldLabelKey, + RaRecord, + SourceContextProvider, + useResourceContext, + useSourceContext, +} from 'ra-core'; import { SimpleFormIteratorClasses } from './useSimpleFormIteratorStyles'; import { useSimpleFormIterator } from './useSimpleFormIterator'; @@ -74,19 +80,23 @@ export const SimpleFormIteratorItem = React.forwardRef( ? getItemLabel(index) : getItemLabel; + const parentSourceContext = useSourceContext(); const sourceContext = useMemo( () => ({ getSource: (source: string) => source ? `${member}.${source}` : member, getLabel: (source: string) => { // remove digits, e.g. 'book.authors.2.categories.3.identifier.name' => 'book.authors.categories.identifier.name' - return `resources.${resource}.fields.${member.replace( + const itemSource = `${member.replace( /\.\d+/g, '' )}.${source}`; + return parentSourceContext + ? parentSourceContext.getLabel(itemSource) + : getResourceFieldLabelKey(resource, itemSource); }, }), - [member, resource] + [member, parentSourceContext, resource] ); return ( diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index 3d3b1229233..f1a4f44fd49 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -7,7 +7,9 @@ import { FormGroupContextProvider, RaRecord, SourceContextProvider, + getResourceFieldLabelKey, useResourceContext, + useSourceContext, useTranslatableContext, } from 'ra-core'; @@ -21,16 +23,18 @@ export const TranslatableInputsTabContent = ( const { children, groupKey = '', locale, ...other } = props; const resource = useResourceContext(props); const { selectedLocale } = useTranslatableContext(); + const parentSourceContext = useSourceContext(); const sourceContext = useMemo( () => ({ getSource: (source: string) => `${source}.${locale}`, - getLabel: (source: string) => - `resources.${resource}.fields.${source.replace( - `.${locale}`, - '' - )}`, + getLabel: (source: string) => { + const itemSource = source.replace(`.${locale}`, ''); + return parentSourceContext + ? parentSourceContext.getLabel(itemSource) + : getResourceFieldLabelKey(resource, itemSource); + }, }), - [locale, resource] + [locale, parentSourceContext, resource] ); return ( diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx index 5634f114716..4162de568b0 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx @@ -10,8 +10,9 @@ import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import { FormGroupsProvider, - LabelPrefixContextProvider, ListFilterContextValue, + SourceContextProvider, + SourceContextValue, useListContext, useResourceContext, } from 'ra-core'; @@ -133,8 +134,17 @@ export const FilterFormBase = (props: FilterFormBaseProps) => { [hideFilter] ); + const sourceContext = React.useMemo( + () => ({ + getSource: (source: string) => source, + getLabel: (source: string) => + `resources.${resource}.fields.${source}`, + }), + [resource] + ); + return ( - + { ))}
    - + ); }; From 6d69112ae0633c25192879185e6e432618492799 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:43:56 +0100 Subject: [PATCH 24/28] Simplify TranslatableInputsTabContent getLabel --- .../src/input/TranslatableInputsTabContent.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index f1a4f44fd49..a5ca53fb311 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -28,10 +28,9 @@ export const TranslatableInputsTabContent = ( () => ({ getSource: (source: string) => `${source}.${locale}`, getLabel: (source: string) => { - const itemSource = source.replace(`.${locale}`, ''); return parentSourceContext - ? parentSourceContext.getLabel(itemSource) - : getResourceFieldLabelKey(resource, itemSource); + ? parentSourceContext.getLabel(source) + : getResourceFieldLabelKey(resource, source); }, }), [locale, parentSourceContext, resource] From 84c71d86cb30eaa2b2662a0b9e0c94233ab82b7b Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:18:24 +0100 Subject: [PATCH 25/28] Handle scalar array inputs --- packages/ra-core/src/form/useInput.ts | 10 +++++ .../src/i18n/TestTranslationProvider.tsx | 39 ++++++++++------- .../input/ArrayInput/ArrayInput.stories.tsx | 43 ++++++++++++++++++- .../ArrayInput/SimpleFormIteratorItem.tsx | 10 +++-- 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index f15413e0d22..78e00fcd533 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -45,6 +45,16 @@ export const useInput = ( const formGroups = useFormGroups(); const record = useRecordContext(); + if ( + !source && + props.label == null && + process.env.NODE_ENV === 'development' + ) { + console.warn( + 'If your input source is empty, you must provide a label prop.' + ); + } + useEffect(() => { if (!formGroups || formGroupName == null) { return; diff --git a/packages/ra-core/src/i18n/TestTranslationProvider.tsx b/packages/ra-core/src/i18n/TestTranslationProvider.tsx index e89cc533a23..af5c8b366e0 100644 --- a/packages/ra-core/src/i18n/TestTranslationProvider.tsx +++ b/packages/ra-core/src/i18n/TestTranslationProvider.tsx @@ -2,28 +2,35 @@ import * as React from 'react'; import lodashGet from 'lodash/get'; import { I18nContextProvider } from './I18nContextProvider'; +import { I18nProvider } from '../types'; export const TestTranslationProvider = ({ translate, messages, children, }: any) => ( - { - const message = lodashGet(messages, key); - return message - ? typeof message === 'function' - ? message(options) - : message - : options?._ || key; - } - : translate, - changeLocale: () => Promise.resolve(), - getLocale: () => 'en', - }} - > + {children} ); + +export const testI18nProvider = ({ + translate, + messages, +}: { + translate?: I18nProvider['translate']; + messages?: Record string)>; +}): I18nProvider => ({ + translate: messages + ? (key, options) => { + const message = lodashGet(messages, key); + return message + ? typeof message === 'function' + ? message(options) + : message + : options?._ || key; + } + : translate, + changeLocale: () => Promise.resolve(), + getLocale: () => 'en', +}); diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx index 1ac0385db24..e774862d697 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Admin } from 'react-admin'; -import { required, Resource } from 'ra-core'; +import { required, Resource, testI18nProvider } from 'ra-core'; import { createMemoryHistory } from 'history'; import { InputAdornment } from '@mui/material'; @@ -167,6 +167,47 @@ export const Scalar = () => ( ); +export const ScalarI18n = () => ( + + ( + { + console.log(data); + }, + }} + > + + + + + + + + + + )} + /> + +); + const order = { id: 1, date: '2022-08-30', diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx index 18fc6aa36bf..17b4db5542d 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIteratorItem.tsx @@ -87,10 +87,12 @@ export const SimpleFormIteratorItem = React.forwardRef( source ? `${member}.${source}` : member, getLabel: (source: string) => { // remove digits, e.g. 'book.authors.2.categories.3.identifier.name' => 'book.authors.categories.identifier.name' - const itemSource = `${member.replace( - /\.\d+/g, - '' - )}.${source}`; + const sanitizedMember = member.replace(/\.\d+/g, ''); + // source may be empty for scalar values arrays + const itemSource = source + ? `${sanitizedMember}.${source}` + : sanitizedMember; + return parentSourceContext ? parentSourceContext.getLabel(itemSource) : getResourceFieldLabelKey(resource, itemSource); From 5496cca68266cd8cec92d1033f90ea65a66effde Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:16:17 +0100 Subject: [PATCH 26/28] Apply suggestions from code review Co-authored-by: Francois Zaninotto --- packages/ra-core/src/form/useInput.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts index 78e00fcd533..f0b56bf4356 100644 --- a/packages/ra-core/src/form/useInput.ts +++ b/packages/ra-core/src/form/useInput.ts @@ -51,7 +51,7 @@ export const useInput = ( process.env.NODE_ENV === 'development' ) { console.warn( - 'If your input source is empty, you must provide a label prop.' + 'Input components require either a source or a label prop.' ); } From 50258d5e48366a6520b2a3341172badf34dbcaf2 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:22:01 +0100 Subject: [PATCH 27/28] Update documentation to remove passing rest parameters --- docs/SimpleFormIterator.md | 2 -- docs/Upgrade.md | 2 -- examples/simple/src/posts/PostCreate.tsx | 3 +-- examples/simple/src/posts/PostEdit.tsx | 3 +-- .../ra-core/src/form/FormDataConsumer.spec.tsx | 14 ++++++-------- packages/ra-core/src/form/FormDataConsumer.tsx | 13 ++++++------- .../ConfigurationInputsFromFieldDefinition.tsx | 3 +-- 7 files changed, 15 insertions(+), 25 deletions(-) diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md index 769ab2d9e4b..df12e28a5fe 100644 --- a/docs/SimpleFormIterator.md +++ b/docs/SimpleFormIterator.md @@ -129,13 +129,11 @@ const PostEdit = () => ( {({ formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput - ...rest }) => scopedFormData && scopedFormData.name ? ( ) : null } diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 7091557140c..d1470ddcb36 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -237,14 +237,12 @@ const PostEdit = () => ( formData, // The whole form data scopedFormData, // The data for this item of the ArrayInput - getSource, - ...rest }) => scopedFormData && getSource && scopedFormData.name ? ( ) : null } diff --git a/examples/simple/src/posts/PostCreate.tsx b/examples/simple/src/posts/PostCreate.tsx index 2aef45fb01c..80e4c62f4f0 100644 --- a/examples/simple/src/posts/PostCreate.tsx +++ b/examples/simple/src/posts/PostCreate.tsx @@ -163,7 +163,7 @@ const PostCreate = () => { /> - {({ scopedFormData, ...rest }) => + {({ scopedFormData }) => scopedFormData && scopedFormData.user_id ? ( { name: 'Co-Writer', }, ]} - {...rest} label="Role" /> ) : null diff --git a/examples/simple/src/posts/PostEdit.tsx b/examples/simple/src/posts/PostEdit.tsx index 2c0e32e3900..73bdfe0c7ed 100644 --- a/examples/simple/src/posts/PostEdit.tsx +++ b/examples/simple/src/posts/PostEdit.tsx @@ -160,7 +160,7 @@ const PostEdit = () => { - {({ scopedFormData, ...rest }) => + {({ scopedFormData }) => scopedFormData && scopedFormData.user_id ? ( { }, ]} helperText={false} - {...rest} /> ) : null } diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 72c363d708b..4d03a3a4d3e 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -40,10 +40,10 @@ describe('FormDataConsumerView', () => { - {({ formData, ...rest }) => { + {({ formData }) => { globalFormData = formData; - return ; + return ; }} @@ -61,10 +61,8 @@ describe('FormDataConsumerView', () => { - {({ formData, ...rest }) => - !formData.hi ? ( - - ) : null + {({ formData }) => + !formData.hi ? : null } @@ -95,11 +93,11 @@ describe('FormDataConsumerView', () => { - {({ formData, scopedFormData, ...rest }) => { + {({ scopedFormData }) => { globalScopedFormData = scopedFormData; return scopedFormData && scopedFormData.name ? ( - + ) : null; }} diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index fd7e1c75ba5..f478e41a116 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -16,8 +16,8 @@ import { useWrappedSource } from '../core'; * > * * - * {({ formData, ...rest }) => formData.hasEmail && - * + * {({ formData }) => formData.hasEmail && + * * } * * @@ -31,11 +31,10 @@ import { useWrappedSource } from '../core'; * * * > - * {({ formData, ...rest }) => + * {({ formData }) => * * } * @@ -65,7 +64,7 @@ export const FormDataConsumerView = < >( props: Props ) => { - const { children, form, formData, source, ...rest } = props; + const { children, formData, source } = props; let ret; const finalSource = useWrappedSource(source); @@ -75,9 +74,9 @@ export const FormDataConsumerView = < // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) if (matches) { const scopedFormData = get(formData, matches[0]); - ret = children({ formData, scopedFormData, ...rest }); + ret = children({ formData, scopedFormData }); } else { - ret = children({ formData, ...rest }); + ret = children({ formData }); } return ret === undefined ? null : ret; diff --git a/packages/ra-no-code/src/ResourceConfiguration/ConfigurationInputsFromFieldDefinition.tsx b/packages/ra-no-code/src/ResourceConfiguration/ConfigurationInputsFromFieldDefinition.tsx index 83bcab9c0b0..c2b61b9c9e9 100644 --- a/packages/ra-no-code/src/ResourceConfiguration/ConfigurationInputsFromFieldDefinition.tsx +++ b/packages/ra-no-code/src/ResourceConfiguration/ConfigurationInputsFromFieldDefinition.tsx @@ -36,7 +36,7 @@ export const ConfigurationInputsFromFieldDefinition = ({ choices={ReferenceSelectionChoice} /> - {({ formData, ...rest }) => { + {({ formData }) => { const resourceName = get( formData, `${sourcePrefix}.props.reference` @@ -54,7 +54,6 @@ export const ConfigurationInputsFromFieldDefinition = ({ field.props.label || field.props.source, }))} - {...rest} /> ); }} From 3894f03fcb6777c4f876c2206f6d55edd799d2bb Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:09:34 +0100 Subject: [PATCH 28/28] Better ArrayInput i18n story --- .../src/input/ArrayInput/ArrayInput.stories.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx index e774862d697..45e35c5d0dc 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.stories.tsx @@ -177,6 +177,7 @@ export const ScalarI18n = () => ( books: { fields: { tags: 'Some tags', + tag: 'A tag', }, }, }, @@ -198,7 +199,11 @@ export const ScalarI18n = () => ( - +