diff --git a/docs/Fields.md b/docs/Fields.md index 32753a25c9d..1f035c028a5 100644 --- a/docs/Fields.md +++ b/docs/Fields.md @@ -501,22 +501,25 @@ export const UserList = () => ( **Tip**: In such custom fields, the `source` is optional. React-admin uses it to determine which column to use for sorting when the column header is clicked. In case you use the `source` property for additional purposes, the sorting can be overridden by the `sortBy` property on any `Field` component. -If you build a reusable field accepting a `source` props, you will probably want to support deep field sources (e.g. source values like `author.name`). Use [lodash/get](https://www.npmjs.com/package/lodash.get) to replace the simple object lookup. For instance, for a Text field: +If you build a reusable field accepting a `source` props, you will probably want to support deep field sources (e.g. source values like `author.name`). Use the [`useFieldValue` hook](/useFieldValue.md) to replace the simple object lookup. For instance, for a Text field: ```diff import * as React from 'react'; -+import get from 'lodash/get'; -import { useRecordContext } from 'react-admin'; - -const TextField = ({ source }) => { - const record = useRecordContext(); -- return record ? {record[source]} : null; -+ return record ? {get(record, source)} : null; +-import { useRecordContext } from 'react-admin'; ++import { useFieldValue } from 'react-admin'; + +const TextField = (props) => { +- const record = useRecordContext(); ++ const value = useFieldValue(props); +- return record ? {record[props.source]} : null; ++ return {value} : null; } export default TextField; ``` +**Tip**: Note that when using `useFieldValue`, you don't need to check that `record` is defined. + ## Hiding A Field Based On The Value Of Another In a Show view, you may want to display or hide fields based on the value of another field - for instance, show an `email` field only if the `hasEmail` boolean field is `true`. diff --git a/docs/Reference.md b/docs/Reference.md index c17f038a678..2707812350e 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -236,6 +236,9 @@ title: "Index" * [`useEditContext`](./useEditContext.md) * [`useEditController`](./useEditController.md) +**- F -** +* [`useFieldValue`](./useFieldValue.md) + **- G -** * [`useGetIdentity`](./useGetIdentity.md) * [`useGetList`](./useGetList.md) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 0c74b23988b..f47b872c6d2 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -437,6 +437,28 @@ const PostEdit = () => ( ); ``` +## Fields Components Requires The `source` Prop + +The `FieldProps` interface now requires the `source` prop to be defined. As a consequence, all the default fields components also require the `source` prop to be defined. +This impacts custom fields that typed their props with the `FieldProps` interface. If your custom field is not meant to be used in a ``, you may declare the `source` prop optional: + +```diff +import { FieldProps, useRecordContext } from 'react-admin'; + +-const AvatarField = (props: FieldProps) => { ++const AvatarField = (props: Omit) => { + const record = useRecordContext(); + if (!record) return null; + return ( + + ); +} +``` + ## 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. diff --git a/docs/navigation.html b/docs/navigation.html index ed7d5ba9053..a12f864e8b7 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -179,6 +179,7 @@
  • <TranslatableFields>
  • <UrlField>
  • <WrapperField>
  • +
  • useFieldValue
    • Inputs
      diff --git a/docs/useFieldValue.md b/docs/useFieldValue.md new file mode 100644 index 00000000000..728d5428d4e --- /dev/null +++ b/docs/useFieldValue.md @@ -0,0 +1,83 @@ +--- +layout: default +title: "useFieldValue" +--- + +# `useFieldValue` + +A hook that gets the value of a field of the current record. It gets the current record from the context or use the one provided as a prop. It supports deep sources such as `name.fr`. + +## Usage + +Here is an example `TextField` component: + +```tsx +// In TextField.tsx +import * as React from 'react'; +import { useFieldValue, type FieldProps } from 'react-admin'; + +export const TextField = (props: FieldProps) => { + const value = useFieldValue(props); + return {value}; +} + +// In PostShow.tsx +import { Show, SimpleShowLayout } from 'react-admin'; +import { TextField } from './TextField.tsx'; + +export const PostShow = () => ( + + + + + +); +``` + +## Params + +### `source` + +The name of the property on the record object that contains the value to display. Can be a deep path. + +```tsx +import * as React from 'react'; +import { useFieldValue } from 'react-admin'; + +export const CustomerCard = () => { + const firstName = useFieldValue({ source: 'firstName' }); + const lastName = useFieldValue({ source: 'lastName' }); + return {lastName} {firstName}; +} +``` + +### `record` + +The record from which to read the value. Read from the `RecordContext` by default. + + +```tsx +import * as React from 'react'; +import { useFieldValue, useGetOne } from 'react-admin'; + +export const CustomerCard = ({ id }: { id: string }) => { + const { data } = useGetOne('customer', { id }); + const firstName = useFieldValue({ source: 'firstName', record: data }); + const lastName = useFieldValue({ source: 'lastName', record: data }); + return {lastName} {firstName}; +} +``` + +### `defaultValue` + +The value to return when the record does not have a value for the specified `source`. + +```tsx +import * as React from 'react'; +import { useFieldValue } from 'react-admin'; + +export const CustomerStatus = () => { + const status = useFieldValue({ source: 'status', defaultValue: 'active' }); + return {status}; +} +``` diff --git a/examples/demo/src/visitors/AvatarField.tsx b/examples/demo/src/visitors/AvatarField.tsx index e2cadf81407..61a4725f697 100644 --- a/examples/demo/src/visitors/AvatarField.tsx +++ b/examples/demo/src/visitors/AvatarField.tsx @@ -3,7 +3,7 @@ import { Avatar, SxProps } from '@mui/material'; import { FieldProps, useRecordContext } from 'react-admin'; import { Customer } from '../types'; -interface Props extends FieldProps { +interface Props extends Omit, 'source'> { sx?: SxProps; size?: string; } diff --git a/examples/demo/src/visitors/FullNameField.tsx b/examples/demo/src/visitors/FullNameField.tsx index c0a2c2f4686..0dfe465963a 100644 --- a/examples/demo/src/visitors/FullNameField.tsx +++ b/examples/demo/src/visitors/FullNameField.tsx @@ -6,7 +6,7 @@ import { FieldProps, useRecordContext } from 'react-admin'; import AvatarField from './AvatarField'; import { Customer } from '../types'; -interface Props extends FieldProps { +interface Props extends Omit, 'source'> { size?: string; sx?: SxProps; } diff --git a/examples/demo/src/visitors/SegmentsField.tsx b/examples/demo/src/visitors/SegmentsField.tsx index 79bce6572e7..1882901c6cd 100644 --- a/examples/demo/src/visitors/SegmentsField.tsx +++ b/examples/demo/src/visitors/SegmentsField.tsx @@ -9,7 +9,7 @@ const segmentsById = segments.reduce((acc, segment) => { return acc; }, {} as { [key: string]: any }); -const SegmentsField = (_: FieldProps) => { +const SegmentsField = (_: Omit & { source?: string }) => { const translate = useTranslate(); const record = useRecordContext(); if (!record || !record.groups) { diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 8b355605e44..1fb3597db01 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -62,6 +62,7 @@ "clsx": "^1.1.1", "date-fns": "^2.19.0", "eventemitter3": "^4.0.7", + "hotscript": "^1.0.12", "inflection": "~1.12.0", "jsonexport": "^3.2.0", "lodash": "~4.17.5", diff --git a/packages/ra-core/src/i18n/TranslatableContext.ts b/packages/ra-core/src/i18n/TranslatableContext.ts index 1932c5c9471..9e0a6111eed 100644 --- a/packages/ra-core/src/i18n/TranslatableContext.ts +++ b/packages/ra-core/src/i18n/TranslatableContext.ts @@ -5,13 +5,9 @@ export const TranslatableContext = createContext< >(undefined); export interface TranslatableContextValue { - getLabel: GetTranslatableLabel; - getSource: GetTranslatableSource; locales: string[]; selectedLocale: string; selectLocale: SelectTranslatableLocale; } -export type GetTranslatableSource = (field: string, locale?: string) => string; -export type GetTranslatableLabel = (field: string, label?: string) => string; export type SelectTranslatableLocale = (locale: string) => void; diff --git a/packages/ra-core/src/i18n/useTranslatable.ts b/packages/ra-core/src/i18n/useTranslatable.ts index 876ab0c3585..a540939fd52 100644 --- a/packages/ra-core/src/i18n/useTranslatable.ts +++ b/packages/ra-core/src/i18n/useTranslatable.ts @@ -1,8 +1,6 @@ import { useState, useMemo } from 'react'; -import { useResourceContext } from '../core'; import { TranslatableContextValue } from './TranslatableContext'; import { useLocaleState } from './useLocaleState'; -import { useTranslateLabel } from './useTranslateLabel'; /** * Hook supplying the logic to translate a field value in multiple languages. @@ -25,22 +23,14 @@ export const useTranslatable = ( const [localeFromUI] = useLocaleState(); const { defaultLocale = localeFromUI, locales } = options; const [selectedLocale, setSelectedLocale] = useState(defaultLocale); - const resource = useResourceContext({}); - const translateLabel = useTranslateLabel(); 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, selectedLocale, selectLocale: setSelectedLocale, }), - [locales, resource, selectedLocale, translateLabel] + [locales, selectedLocale] ); return context; diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts index e929def61df..2f9d7f89e75 100644 --- a/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.ts @@ -25,7 +25,6 @@ export const getFieldLabelTranslationArgs = ( options?: Args ): TranslationArguments => { if (!options) return ['']; - const { label, defaultLabel, diff --git a/packages/ra-core/src/util/index.ts b/packages/ra-core/src/util/index.ts index d02a278ad07..07dd9b6a2a7 100644 --- a/packages/ra-core/src/util/index.ts +++ b/packages/ra-core/src/util/index.ts @@ -10,6 +10,7 @@ import { getMutationMode } from './getMutationMode'; export * from './getFieldLabelTranslationArgs'; export * from './mergeRefs'; export * from './useEvent'; +export * from './useFieldValue'; export { escapePath, diff --git a/packages/ra-core/src/util/useFieldValue.spec.tsx b/packages/ra-core/src/util/useFieldValue.spec.tsx new file mode 100644 index 00000000000..a47d09a4963 --- /dev/null +++ b/packages/ra-core/src/util/useFieldValue.spec.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useFieldValue, UseFieldValueOptions } from './useFieldValue'; +import { RecordContextProvider } from '../controller'; +import { SourceContextProvider } from '..'; + +describe('useFieldValue', () => { + const Component = (props: UseFieldValueOptions) => { + return
      {useFieldValue(props) ?? 'None'}
      ; + }; + + it('should return undefined if no record is available', async () => { + render(); + + await screen.findByText('None'); + }); + + it('should return the provided defaultValue if no record is available', async () => { + render(); + + await screen.findByText('Molly Millions'); + }); + + it('should return the provided defaultValue if the record does not have a value for the source', async () => { + render( + + + + ); + + await screen.findByText('Peter Riviera'); + }); + + it('should return the field value from the record in RecordContext', async () => { + render( + + + + ); + + await screen.findByText('John Wick'); + }); + + it('should return the field value from the record in props', async () => { + render( + + + + ); + + await screen.findByText('Johnny Silverhand'); + }); + + it('should return the field value from a deep path', async () => { + render( + + + + ); + + await screen.findByText('John'); + }); + + it('should return the field value from the record inside a SourceContext', async () => { + render( + + source, + }} + > + + + + ); + + await screen.findByText('Neuromancien'); + }); +}); diff --git a/packages/ra-core/src/util/useFieldValue.ts b/packages/ra-core/src/util/useFieldValue.ts new file mode 100644 index 00000000000..6050c7c5d05 --- /dev/null +++ b/packages/ra-core/src/util/useFieldValue.ts @@ -0,0 +1,47 @@ +import get from 'lodash/get'; +import { Call, Objects } from 'hotscript'; +import { useRecordContext } from '../controller'; +import { useSourceContext } from '../core'; + +/** + * A hook that gets the value of a field of the current record. + * @param params The hook parameters + * @param params.source The field source + * @param params.record The record to use. Uses the record from the RecordContext if not provided + * @param params.defaultValue The value to return when the field value is empty + * @returns The field value + * + * @example + * const MyField = (props: { source: string }) => { + * const value = useFieldValue(props); + * return {value}; + * } + */ +export const useFieldValue = < + RecordType extends Record = Record +>( + params: UseFieldValueOptions +) => { + const { defaultValue, source } = params; + const sourceContext = useSourceContext(); + const record = useRecordContext(params); + + return get( + record, + sourceContext?.getSource(source) ?? source, + defaultValue + ); +}; + +export interface UseFieldValueOptions< + RecordType extends Record = Record +> { + // FIXME: Find a way to throw a type error when defaultValue is not of RecordType[Source] type + defaultValue?: any; + source: Call extends never + ? AnyString + : Call; + record?: RecordType; +} + +type AnyString = string & {}; diff --git a/packages/ra-ui-materialui/src/field/BooleanField.tsx b/packages/ra-ui-materialui/src/field/BooleanField.tsx index 0cde996d681..fada483a3c9 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.tsx +++ b/packages/ra-ui-materialui/src/field/BooleanField.tsx @@ -2,11 +2,10 @@ import * as React from 'react'; import { styled } from '@mui/material/styles'; import { SvgIconComponent } from '@mui/icons-material'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import DoneIcon from '@mui/icons-material/Done'; import ClearIcon from '@mui/icons-material/Clear'; import { Tooltip, Typography, TypographyProps } from '@mui/material'; -import { useTranslate, useRecordContext } from 'ra-core'; +import { useTranslate, useFieldValue } from 'ra-core'; import { genericMemo } from './genericMemo'; import { FieldProps, fieldPropTypes } from './types'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; @@ -19,7 +18,6 @@ const BooleanFieldImpl = < const { className, emptyText, - source, valueLabelTrue, valueLabelFalse, TrueIcon = DoneIcon, @@ -27,10 +25,8 @@ const BooleanFieldImpl = < looseValue = false, ...rest } = props; - const record = useRecordContext(props); const translate = useTranslate(); - - const value = get(record, source); + const value = useFieldValue(props); const isTruthyValue = value === true || (looseValue && value); let ariaLabel = value ? valueLabelTrue : valueLabelFalse; diff --git a/packages/ra-ui-materialui/src/field/ChipField.tsx b/packages/ra-ui-materialui/src/field/ChipField.tsx index 152280d2478..679d6480c66 100644 --- a/packages/ra-ui-materialui/src/field/ChipField.tsx +++ b/packages/ra-ui-materialui/src/field/ChipField.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { styled } from '@mui/material/styles'; -import get from 'lodash/get'; import Chip, { ChipProps } from '@mui/material/Chip'; import Typography from '@mui/material/Typography'; import clsx from 'clsx'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -15,9 +14,8 @@ const ChipFieldImpl = < >( props: ChipFieldProps ) => { - const { className, source, emptyText, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source); + const { className, emptyText, ...rest } = props; + const value = useFieldValue(props); const translate = useTranslate(); if (value == null && emptyText) { diff --git a/packages/ra-ui-materialui/src/field/DateField.tsx b/packages/ra-ui-materialui/src/field/DateField.tsx index 45e59220089..1ba6b509c1c 100644 --- a/packages/ra-ui-materialui/src/field/DateField.tsx +++ b/packages/ra-ui-materialui/src/field/DateField.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import { Typography, TypographyProps } from '@mui/material'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -44,7 +43,6 @@ const DateFieldImpl = < options, showTime = false, showDate = true, - source, transform = defaultTransform, ...rest } = props; @@ -56,12 +54,7 @@ const DateFieldImpl = < ); } - const record = useRecordContext(props); - if (!record) { - return null; - } - - const value = get(record, source) as any; + const value = useFieldValue(props); if (value == null || value === '') { return emptyText ? ( ( props: EmailFieldProps ) => { - const { className, source, emptyText, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source); + const { className, emptyText, ...rest } = props; + const value = useFieldValue(props); const translate = useTranslate(); if (value == null) { diff --git a/packages/ra-ui-materialui/src/field/FileField.tsx b/packages/ra-ui-materialui/src/field/FileField.tsx index 9e6c5972464..ed981d45ba2 100644 --- a/packages/ra-ui-materialui/src/field/FileField.tsx +++ b/packages/ra-ui-materialui/src/field/FileField.tsx @@ -3,7 +3,8 @@ import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import get from 'lodash/get'; import Typography from '@mui/material/Typography'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; +import { Call, Objects } from 'hotscript'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -31,7 +32,6 @@ export const FileField = < const { className, emptyText, - source, title, src, target, @@ -40,8 +40,12 @@ export const FileField = < rel, ...rest } = props; - const record = useRecordContext(props); - const sourceValue = get(record, source); + const sourceValue = useFieldValue(props); + const titleValue = + useFieldValue({ + ...props, + source: title, + })?.toString() ?? title; const translate = useTranslate(); if (!sourceValue) { @@ -87,8 +91,6 @@ export const FileField = < ); } - const titleValue = get(record, title)?.toString() || title; - return ( = Record > extends FieldProps { src?: string; - title?: string; + title?: Call extends never + ? AnyString + : Call; target?: string; download?: boolean | string; ping?: string; rel?: string; sx?: SxProps; } +type AnyString = string & {}; FileField.propTypes = { ...fieldPropTypes, diff --git a/packages/ra-ui-materialui/src/field/FunctionField.tsx b/packages/ra-ui-materialui/src/field/FunctionField.tsx index 119ff5c1829..2ae5696020d 100644 --- a/packages/ra-ui-materialui/src/field/FunctionField.tsx +++ b/packages/ra-ui-materialui/src/field/FunctionField.tsx @@ -48,7 +48,8 @@ FunctionField.propTypes = { export interface FunctionFieldProps< RecordType extends Record = any -> extends FieldProps, +> extends Omit, 'source'>, Omit { + source?: string; render: (record: RecordType, source?: string) => ReactNode; } diff --git a/packages/ra-ui-materialui/src/field/ImageField.tsx b/packages/ra-ui-materialui/src/field/ImageField.tsx index 7a93910a8b5..eeba8620b41 100644 --- a/packages/ra-ui-materialui/src/field/ImageField.tsx +++ b/packages/ra-ui-materialui/src/field/ImageField.tsx @@ -3,7 +3,8 @@ import { styled } from '@mui/material/styles'; import { Box, Typography } from '@mui/material'; import PropTypes from 'prop-types'; import get from 'lodash/get'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; +import { Call, Objects } from 'hotscript'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -14,9 +15,13 @@ export const ImageField = < >( props: ImageFieldProps ) => { - const { className, emptyText, source, src, title, ...rest } = props; - const record = useRecordContext(props); - const sourceValue = get(record, source); + const { className, emptyText, src, title, ...rest } = props; + const sourceValue = useFieldValue(props); + const titleValue = + useFieldValue({ + ...props, + source: title, + })?.toString() ?? title; const translate = useTranslate(); if (!sourceValue) { @@ -62,8 +67,6 @@ export const ImageField = < ); } - const titleValue = get(record, title)?.toString() || title; - return ( = Record > extends FieldProps { src?: string; - title?: string; + title?: Call extends never + ? AnyString + : Call; sx?: SxProps; } + +type AnyString = string & {}; diff --git a/packages/ra-ui-materialui/src/field/NumberField.tsx b/packages/ra-ui-materialui/src/field/NumberField.tsx index 4a765b68ee0..a1ddb3b0e66 100644 --- a/packages/ra-ui-materialui/src/field/NumberField.tsx +++ b/packages/ra-ui-materialui/src/field/NumberField.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import Typography, { TypographyProps } from '@mui/material/Typography'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -51,13 +50,8 @@ const NumberFieldImpl = < transform = defaultTransform, ...rest } = props; - const record = useRecordContext(props); const translate = useTranslate(); - - if (!record) { - return null; - } - let value: any = get(record, source); + let value = useFieldValue(props); if (value == null) { return emptyText ? ( diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx index 8a34cba3dff..79044a7ad0c 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { ReactNode } from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import { Typography, SxProps } from '@mui/material'; import { styled } from '@mui/material/styles'; import ErrorIcon from '@mui/icons-material/Error'; @@ -18,6 +17,7 @@ import { useResourceDefinition, useTranslate, RaRecord, + useFieldValue, } from 'ra-core'; import { UseQueryOptions } from '@tanstack/react-query'; @@ -65,7 +65,8 @@ export const ReferenceField = < ) => { const { source, emptyText, link = 'edit', ...rest } = props; const record = useRecordContext(props); - const id = get(record, source); + const id = useFieldValue(props); + const translate = useTranslate(); return id == null ? ( diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx index 025f5a6c378..5ba917e5bf2 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx @@ -100,9 +100,10 @@ export const ReferenceManyCount = ( }; export interface ReferenceManyCountProps - extends FieldProps, + extends Omit, 'source'>, Omit { reference: string; + source?: string; target: string; sort?: SortPayload; filter?: any; diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx index a02ce9336dd..f300391aff8 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx @@ -106,7 +106,7 @@ export const ReferenceManyField = < export interface ReferenceManyFieldProps< RecordType extends Record = Record -> extends FieldProps { +> extends Omit, 'source'> { children: ReactNode; filter?: FilterPayload; page?: number; @@ -114,6 +114,7 @@ export interface ReferenceManyFieldProps< perPage?: number; reference: string; sort?: SortPayload; + source?: string; target: string; } diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx index def6d0f4f20..3b082efbd3a 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx @@ -93,6 +93,7 @@ export const ReferenceOneField = < reference={reference} refetch={refetch} error={error} + source={source} > {children} diff --git a/packages/ra-ui-materialui/src/field/RichTextField.tsx b/packages/ra-ui-materialui/src/field/RichTextField.tsx index b4d7cf5430a..0b3974243f4 100644 --- a/packages/ra-ui-materialui/src/field/RichTextField.tsx +++ b/packages/ra-ui-materialui/src/field/RichTextField.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; import Typography, { TypographyProps } from '@mui/material/Typography'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import purify from 'dompurify'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; @@ -32,13 +31,11 @@ const RichTextFieldImpl = < const { className, emptyText, - source, stripTags = false, purifyOptions, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source)?.toString(); + const value = useFieldValue(props); const translate = useTranslate(); return ( diff --git a/packages/ra-ui-materialui/src/field/SelectField.tsx b/packages/ra-ui-materialui/src/field/SelectField.tsx index ac8f887b07f..366a7240f32 100644 --- a/packages/ra-ui-materialui/src/field/SelectField.tsx +++ b/packages/ra-ui-materialui/src/field/SelectField.tsx @@ -1,12 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import get from 'lodash/get'; -import { - ChoicesProps, - useChoices, - useRecordContext, - useTranslate, -} from 'ra-core'; +import { ChoicesProps, useChoices, useFieldValue, useTranslate } from 'ra-core'; import { Typography, TypographyProps } from '@mui/material'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; @@ -83,15 +77,14 @@ const SelectFieldImpl = < const { className, emptyText, - source, choices, optionValue = 'id', optionText = 'name', translateChoice = true, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source); + const value = useFieldValue(props); + const { getChoiceText, getChoiceValue } = useChoices({ optionText, optionValue, diff --git a/packages/ra-ui-materialui/src/field/TextField.tsx b/packages/ra-ui-materialui/src/field/TextField.tsx index 40d17239a2e..3d6ae67d0b7 100644 --- a/packages/ra-ui-materialui/src/field/TextField.tsx +++ b/packages/ra-ui-materialui/src/field/TextField.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { ElementType } from 'react'; -import get from 'lodash/get'; import Typography, { TypographyProps } from '@mui/material/Typography'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { FieldProps, fieldPropTypes } from './types'; @@ -13,10 +12,9 @@ const TextFieldImpl = < >( props: TextFieldProps ) => { - const { className, source, emptyText, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source)?.toString(); + const { className, emptyText, ...rest } = props; const translate = useTranslate(); + const value = useFieldValue(props); return ( englishMessages); -const Wrapper = ({ children }) => ( - - - {children} - +const Wrapper = ({ + children, + ...props +}: Omit & { children: React.ReactNode }) => ( + + + + {children} + + ); export const Basic = () => ( + + + , + , + + +); + +export const WithoutI18nProvider = () => ( , @@ -61,7 +76,7 @@ export const Basic = () => ( ); export const SingleField = () => ( - + @@ -95,7 +110,7 @@ const Selector = () => { }; export const CustomSelector = () => ( - + }> @@ -104,13 +119,22 @@ export const CustomSelector = () => ( ); export const NestedFields = () => ( - + ); +export const WithLabels = () => ( + + + , + , + + +); + const dataProvider = fakeRestDataProvider({ ngos: defaultData, }); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx index 8d15e713e3b..d56545ebcfc 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx @@ -7,6 +7,7 @@ import { UseTranslatableOptions, RaRecord, useRecordContext, + useResourceContext, } from 'ra-core'; import { TranslatableFieldsTabs } from './TranslatableFieldsTabs'; import { TranslatableFieldsTabContent } from './TranslatableFieldsTabContent'; @@ -74,10 +75,10 @@ export const TranslatableFields = ( groupKey = '', selector = , children, - resource, className, } = props; const record = useRecordContext(props); + const resource = useResourceContext(props); const context = useTranslatable({ defaultLocale, locales }); return ( diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx index c1d5c645018..f2f625606b0 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx @@ -1,13 +1,15 @@ import * as React from 'react'; import { styled } from '@mui/material/styles'; +import { Children, isValidElement, ReactElement, ReactNode } from 'react'; import { - Children, - cloneElement, - isValidElement, - ReactElement, - ReactNode, -} from 'react'; -import { useTranslatableContext, RaRecord } from 'ra-core'; + useTranslatableContext, + RaRecord, + SourceContextProvider, + useSourceContext, + getResourceFieldLabelKey, + RecordContextProvider, + ResourceContextProvider, +} from 'ra-core'; import { Labeled } from '../Labeled'; /** @@ -26,8 +28,22 @@ export const TranslatableFieldsTabContent = ( className, ...other } = props; - const { selectedLocale, getLabel, getSource } = useTranslatableContext(); + const { selectedLocale } = useTranslatableContext(); + const parentSourceContext = useSourceContext(); + const sourceContext = React.useMemo( + () => ({ + getSource: (source: string) => + parentSourceContext + ? parentSourceContext.getSource(`${source}.${locale}`) + : `${source}.${locale}`, + getLabel: (source: string) => + parentSourceContext + ? parentSourceContext.getLabel(source) + : getResourceFieldLabelKey(resource, source), + }), + [locale, parentSourceContext, resource] + ); const addLabel = Children.count(children) > 1; return ( @@ -41,34 +57,28 @@ export const TranslatableFieldsTabContent = ( > {Children.map(children, field => field && isValidElement(field) ? ( -
      - {addLabel ? ( - - {cloneElement(field, { - ...field.props, - label: getLabel(field.props.source), - record, - source: getSource( - field.props.source, - locale - ), - })} - - ) : typeof field === 'string' ? ( - field - ) : ( - cloneElement(field, { - ...field.props, - label: getLabel(field.props.source), - record, - source: getSource(field.props.source, locale), - }) - )} -
      + + + +
      + {addLabel ? ( + + {field} + + ) : ( + field + )} +
      +
      +
      +
      ) : null )}
      diff --git a/packages/ra-ui-materialui/src/field/UrlField.tsx b/packages/ra-ui-materialui/src/field/UrlField.tsx index c495b7ac89a..7e60dd34d67 100644 --- a/packages/ra-ui-materialui/src/field/UrlField.tsx +++ b/packages/ra-ui-materialui/src/field/UrlField.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { AnchorHTMLAttributes } from 'react'; -import get from 'lodash/get'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { Typography, Link } from '@mui/material'; -import { useRecordContext, useTranslate } from 'ra-core'; +import { useFieldValue, useTranslate } from 'ra-core'; import { FieldProps, fieldPropTypes } from './types'; import { genericMemo } from './genericMemo'; @@ -12,9 +11,8 @@ const UrlFieldImpl = < >( props: UrlFieldProps ) => { - const { className, emptyText, source, ...rest } = props; - const record = useRecordContext(props); - const value = get(record, source); + const { className, emptyText, ...rest } = props; + const value = useFieldValue(props); const translate = useTranslate(); if (value == null) { diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts index 25269873126..847e34a0f5c 100644 --- a/packages/ra-ui-materialui/src/field/types.ts +++ b/packages/ra-ui-materialui/src/field/types.ts @@ -58,7 +58,7 @@ export interface FieldProps< * * ); */ - source?: Call extends never + source: Call extends never ? AnyString : Call; diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx index a5ca53fb311..5f40d445648 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -26,7 +26,10 @@ export const TranslatableInputsTabContent = ( const parentSourceContext = useSourceContext(); const sourceContext = useMemo( () => ({ - getSource: (source: string) => `${source}.${locale}`, + getSource: (source: string) => + parentSourceContext + ? parentSourceContext.getSource(`${source}.${locale}`) + : `${source}.${locale}`, getLabel: (source: string) => { return parentSourceContext ? parentSourceContext.getLabel(source) diff --git a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx index 0cd5498a687..2143e2f2248 100644 --- a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx @@ -12,6 +12,7 @@ import { ReferenceArrayField, TextField, UrlField, + ArrayFieldProps, } from '../field'; export const listFieldTypes = { @@ -25,15 +26,22 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} }, array: { // eslint-disable-next-line react/display-name - component: ({ children, ...props }) => ( - - - 0 && children[0].props.source} - /> - - - ), + component: ({ children, ...props }: ArrayFieldProps) => { + const childrenArray = React.Children.toArray(children); + return ( + + + 0 && + React.isValidElement(childrenArray[0]) && + childrenArray[0].props.source + } + /> + + + ); + }, representation: (props, children) => `