From 3c0692eddaea8c085d2bc9d79420c58baded59bc Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 26 Nov 2024 15:35:47 +0100 Subject: [PATCH 01/33] Add TextArrayInput to edit arrays of strings --- docs/ArrayInput.md | 1 + docs/Inputs.md | 2 +- docs/Reference.md | 1 + docs/TextArrayInput.md | 110 ++++++ docs/img/TextArrayInput.mp4 | Bin 0 -> 138303 bytes docs/navigation.html | 1 + .../src/input/TextArrayInput.stories.tsx | 322 ++++++++++++++++++ .../src/input/TextArrayInput.tsx | 129 +++++++ packages/ra-ui-materialui/src/input/index.ts | 1 + 9 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 docs/TextArrayInput.md create mode 100644 docs/img/TextArrayInput.mp4 create mode 100644 packages/ra-ui-materialui/src/input/TextArrayInput.stories.tsx create mode 100644 packages/ra-ui-materialui/src/input/TextArrayInput.tsx diff --git a/docs/ArrayInput.md b/docs/ArrayInput.md index 8d048781fb4..1dc23c2596a 100644 --- a/docs/ArrayInput.md +++ b/docs/ArrayInput.md @@ -38,6 +38,7 @@ To edit arrays of data embedded inside a record, `` creates a list o } ``` +**Tip**: If you need to edit an array of *strings*, like a list of email addresses or a list of tags, you should use a [``](./TextArrayInput.md) instead. `` expects a single child, which must be a *form iterator* component. A form iterator is a component rendering a field array (the object returned by react-hook-form's [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray)). For instance, [the `` component](./SimpleFormIterator.md) displays an array of react-admin Inputs in an unordered list (`
    `), one sub-form by list item (`
  • `). It also provides controls for adding and removing a sub-record. diff --git a/docs/Inputs.md b/docs/Inputs.md index 41e87eb5437..30186752cd0 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -79,7 +79,7 @@ React-admin provides a set of Input components, each one designed for a specific | Tree node | `42` | [``](./TreeInput.md) | | Foreign key | `42` | [``](./ReferenceInput.md) | | Array of objects | `[{ item: 'jeans', qty: 3 }, { item: 'shirt', qty: 1 }]` | [``](./ArrayInput.md) | -| Array of Enums | `['foo', 'bar']` | [``](./SelectArrayInput.md), [``](./AutocompleteArrayInput.md), [``](./CheckboxGroupInput.md), [``](./DualListInput.md) | +| Array of Enums | `['foo', 'bar']` | [``](./TextArrayinput.md), [``](./SelectArrayInput.md), [``](./AutocompleteArrayInput.md), [``](./CheckboxGroupInput.md), [``](./DualListInput.md) | | Array of foreign keys | `[42, 43]` | [``](./ReferenceArrayInput.md) | | Translations | `{ en: 'Hello', fr: 'Bonjour' }` | [``](./TranslatableInputs.md) | | Related records | `[{ id: 42, title: 'Hello' }, { id: 43, title: 'World' }]` | [``](./ReferenceManyInput.md), [``](./ReferenceManyToManyInput.md), [``](./ReferenceNodeInput.md), [``](./ReferenceOneInput.md) | diff --git a/docs/Reference.md b/docs/Reference.md index 3983cda09a1..0759ec1b43e 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -188,6 +188,7 @@ title: "Index" * [``](./TabbedForm.md) * [``](./TabbedForm.md#versioning) * [``](./TabbedShowLayout.md) +* [``](./TextArrayInput.md) * [``](./TextField.md) * [``](./TextInput.md) * [``](./TimeInput.md) diff --git a/docs/TextArrayInput.md b/docs/TextArrayInput.md new file mode 100644 index 00000000000..ac8c46ea364 --- /dev/null +++ b/docs/TextArrayInput.md @@ -0,0 +1,110 @@ +--- +layout: default +title: "The TextArrayInput Component" +--- + +# `` + +`` lets you edit an array of strings, like a list of email addresses or a list of tags. It renders as an input where the current values are represented as chips. Users can add or delete new values. + + + + +## Usage + +Use `` to edit an array of strings: + +```jsx +import { Create, SimpleForm, TextArrayInput, TextInput } from 'react-admin'; + +export const EmailCreate = () => ( + + + + + + + +); +``` + +This form will allow users to input multiple email addresses in the `to` field. The resulting email will look like this: + +```jsx +{ + "to": ["jane.smith@example.com", "john.doe@acme.com"], + "subject": "Request for a quote", + "body": "Hi,\n\nI would like to know if you can provide a quote for the following items:\n\n- 100 units of product A\n- 50 units of product B\n- 25 units of product C\n\nBest regards,\n\nJulie\n", + "id": 123, + "date": "2024-11-26T11:37:22.564Z", + "from": "julie.green@example.com", +} +``` + +`` is designed for simple string arrays. For more complex use cases, consider the following alternatives: + +- [``](./SelectArrayInput.md) or [``](./AutocompleteArrayInput.md) if the possible values are limited to a predefined list. +- [``](./ReferenceArrayInput.md) if the possible values are stored in another resource. +- [``](./ArrayInput.md) if the stored value is an array of *objects* instead of an array of strings. + +## Props + +| Prop | Required | Type | Default | Description | +| ------------ | -------- | --------- | ------- | -------------------------------------------------------------------- | +| `options` | Optional | `string[]` | | Optional list of possible values for the input. If provided, the input will suggest these values as the user types. | +| `renderTags` | Optional | `(value, getTagProps) => ReactNode` | | A function to render selected value. | + +`` also accepts the [common input props](./Inputs.md#common-input-props). + +Additional props are passed down to the underlying Material UI [``](https://mui.com/material-ui/react-autocomplete/) component. + +## `options` + +You can make show a list of suggestions to the user by setting the `options` prop: + +```jsx + +``` + +## `renderTags` + +To customize the rendering of the chips, use the `renderTags` prop. This prop is a function that takes two arguments: + +- `value`: The input value (an array of strings) +- `getTagProps`: A props getter for an individual tag. + +```jsx + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } +/> +``` \ No newline at end of file diff --git a/docs/img/TextArrayInput.mp4 b/docs/img/TextArrayInput.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..06fc4ed5e2ae08a0e02371d2485e350a88ecf397 GIT binary patch literal 138303 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
  • <SimpleFormIterator>
  • <SmartRichTextInput>
  • +
  • <TextArrayInput>
  • <TextInput>
  • <TimeInput>
  • <TranslatableInputs>
  • diff --git a/packages/ra-ui-materialui/src/input/TextArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/TextArrayInput.stories.tsx new file mode 100644 index 00000000000..389b1deef8d --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TextArrayInput.stories.tsx @@ -0,0 +1,322 @@ +import * as React from 'react'; +import { required, email, Resource, TestMemoryRouter } from 'ra-core'; +import fakeRestDataProvider from 'ra-data-fakerest'; +import polyglotI18nProvider from 'ra-i18n-polyglot'; +import englishMessages from 'ra-language-english'; +import { Chip } from '@mui/material'; + +import { + AdminUI, + AdminContext, + Create, + List, + SimpleList, + SimpleForm, + ShowGuesser, + TextInput, +} from '../'; +import { TextArrayInput } from './TextArrayInput'; +import { FormInspector } from './common'; + +export default { title: 'ra-ui-materialui/input/TextArrayInput' }; + +const Wrapper = ({ + children, + record = { id: 123, to: ['john@example.com', 'albert@target.dev'] }, +}: { + children: React.ReactNode; + record?: any; +}) => ( + + + {children} + + +); + +export const Basic = () => ( + + + + +); + +export const Disabled = () => ( + + + + +); + +export const ReadOnly = () => ( + + + + +); + +export const DefaultValue = () => ( + + + + +); + +export const HelperText = () => ( + + + + + +); + +export const Label = () => ( + + + + + +); + +export const NonFullWidth = () => ( + + + + +); + +export const Margin = () => ( + + + + + +); + +export const Variant = () => ( + + + + + +); + +export const Validate = () => ( + + { + if (value.some(email())) { + return 'Not an array of valid emails'; + } + return undefined; + }} + /> + +); + +export const Required = () => ( + + + + + +); + +export const Options = () => ( + + + +); + +export const RenderTags = () => ( + + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + /> + +); + +export const Sx = () => ( + + + +); + +export const ExtraProps = () => ( + + + +); + +export const ValueUndefined = () => ( + + + + +); + +export const ValueNull = () => ( + + + + +); + +export const Parse = () => ( + + v.map(v1 => `${v1}@example.com`)} + /> + + +); + +export const Format = () => ( + + v?.map(v1 => v1.replace('@example.com', ''))} + /> + + +); + +const i18nProvider = polyglotI18nProvider(() => englishMessages, 'en'); + +const dataProvider = fakeRestDataProvider({ + emails: [ + { + id: 123, + date: '2024-11-26T11:37:22.564Z', + from: 'julie.green@example.com', + to: ['john.doe@example.com', 'jane.smith@example.com'], + subject: 'Feedback on your website', + body: `Hi, I found a bug on your website. Here is how to reproduce it: +1. Go to the home page +2. Click on the button +3. See the error + +Best regards, + +Julie +`, + }, + { + id: 124, + date: '2024-11-28T11:49:22.009Z', + from: 'julie.green@example.com', + to: ['grace.harris@example.com'], + subject: 'Request for a quote', + body: `Hi, + +I would like to know if you can provide a quote for the following items: + +- 100 units of product A +- 50 units of product B +- 25 units of product C + +Best regards, + +Julie +`, + }, + ], +}); + +export const FullApp = () => ( + + + + ( + + + `to: ${email.to.join(', ')}` + } + secondaryText="%{subject}" + tertiaryText={email => + new Date(email.date).toLocaleDateString() + } + linkType="show" + /> + + )} + show={ShowGuesser} + create={() => ( + + + + + + + + )} + /> + + + +); diff --git a/packages/ra-ui-materialui/src/input/TextArrayInput.tsx b/packages/ra-ui-materialui/src/input/TextArrayInput.tsx new file mode 100644 index 00000000000..071c473690e --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TextArrayInput.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { + Chip, + Autocomplete, + AutocompleteProps, + TextField, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { useInput, FieldTitle } from 'ra-core'; +import { InputHelperText } from './InputHelperText'; +import { CommonInputProps } from './CommonInputProps'; + +export type TextArrayInputProps = CommonInputProps & + Omit< + AutocompleteProps, + 'options' | 'renderInput' | 'renderTags' | 'multiple' | 'freeSolo' + > & + // allow to override options and renderTags + Partial< + Pick< + AutocompleteProps, + 'options' | 'renderTags' + > + >; + +export const TextArrayInput = ({ + className, + disabled, + format, + helperText, + label, + margin, + parse, + readOnly, + size, + source, + sx, + validate, + variant, + ...props +}: TextArrayInputProps) => { + const { + field, + fieldState: { error, invalid }, + id, + isRequired, + } = useInput({ + disabled, + format, + parse, + readOnly, + source, + validate, + ...props, + }); + + const renderHelperText = helperText !== false || invalid; + + return ( + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderInput={params => ( + + ) : null + } + helperText={ + renderHelperText ? ( + + ) : null + } + error={invalid} + variant={variant} + margin={margin} + size={size} + /> + )} + sx={sx} + {...field} + value={field.value || emptyArray} // Autocomplete does not accept null or undefined + onChange={(e, newValue: string[]) => field.onChange(newValue)} + {...props} + disabled={disabled || readOnly} + /> + ); +}; + +const emptyArray = []; + +const PREFIX = 'RaTextArrayInput'; + +const StyledAutocomplete = styled( + Autocomplete, + { + name: PREFIX, + overridesResolver: (props, styles) => styles.root, + } +)(({ theme }) => ({ + minWidth: theme.spacing(20), +})); diff --git a/packages/ra-ui-materialui/src/input/index.ts b/packages/ra-ui-materialui/src/input/index.ts index eba17198ff6..52a7f53824e 100644 --- a/packages/ra-ui-materialui/src/input/index.ts +++ b/packages/ra-ui-materialui/src/input/index.ts @@ -24,6 +24,7 @@ export * from './sanitizeInputRestProps'; export * from './SearchInput'; export * from './SelectArrayInput'; export * from './SelectInput'; +export * from './TextArrayInput'; export * from './TextInput'; export * from './TranslatableInputs'; export * from './TranslatableInputsTabContent'; From 76ddad3e8db945ceec53a0b9c903a420718be20a Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:04:07 +0100 Subject: [PATCH 02/33] Introduce SimpleList rowClick --- docs/SimpleList.md | 21 +- .../src/routing/useGetPathForRecord.ts | 5 + .../src/list/SimpleList/SimpleList.spec.tsx | 139 +++--- .../list/SimpleList/SimpleList.stories.tsx | 165 +++++-- .../src/list/SimpleList/SimpleList.tsx | 435 +++++++++++------- .../src/list/datagrid/Datagrid.tsx | 2 +- .../src/list/datagrid/DatagridBody.tsx | 3 +- .../src/list/datagrid/DatagridRow.tsx | 7 +- .../src/list/datagrid/index.ts | 7 +- packages/ra-ui-materialui/src/list/index.ts | 1 + packages/ra-ui-materialui/src/list/types.ts | 7 + 11 files changed, 525 insertions(+), 267 deletions(-) create mode 100644 packages/ra-ui-materialui/src/list/types.ts diff --git a/docs/SimpleList.md b/docs/SimpleList.md index 2bf275370cf..7a4cdbad752 100644 --- a/docs/SimpleList.md +++ b/docs/SimpleList.md @@ -28,7 +28,7 @@ export const PostList = () => ( primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType={record => record.canEdit ? "edit" : "show"} + rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"} rowSx={record => ({ backgroundColor: record.nb_views >= 500 ? '#efe' : 'white' })} /> @@ -44,7 +44,7 @@ export const PostList = () => ( | `primaryText` | Optional | mixed | record representation | The primary text to display. | | `secondaryText` | Optional | mixed | | The secondary text to display. | | `tertiaryText` | Optional | mixed | | The tertiary text to display. | -| `linkType` | Optional |mixed | `"edit"` | The target of each item click. | +| `rowClick` | Optional |mixed | `"edit"` | The action to trigger when the user clicks on a row. | | `leftAvatar` | Optional | function | | A function returning an `` component to display before the primary text. | | `leftIcon` | Optional | function | | A function returning an `` component to display before the primary text. | | `rightAvatar` | Optional | function | | A function returning an `` component to display after the primary text. | @@ -80,9 +80,9 @@ This prop should be a function returning an `` component. When present, This prop should be a function returning an `` component. When present, the `` renders a `` before the `` -## `linkType` +## `rowClick` -The `` items link to the edition page by default. You can also set the `linkType` prop to `show` directly to link to the `` page instead. +The `` items link to the edition page by default. You can also set the `rowClick` prop to `show` directly to link to the `` page instead. ```jsx import { List, SimpleList } from 'react-admin'; @@ -93,17 +93,18 @@ export const PostList = () => ( primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType="show" + rowClick="show" /> ); ``` -`linkType` accepts the following values: +`rowClick` accepts the following values: -* `linkType="edit"`: links to the edit page. This is the default behavior. -* `linkType="show"`: links to the show page. -* `linkType={false}`: does not create any link. +* `rowClick="edit"`: links to the edit page. This is the default behavior. +* `rowClick="show"`: links to the show page. +* `rowClick={false}`: does not link to anything. +* `rowClick={(id, resource, record) => path}`: path can be any of the above values ## `primaryText` @@ -254,7 +255,7 @@ export const PostList = () => { primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType={record => record.canEdit ? "edit" : "show"} + rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"} /> ) : ( diff --git a/packages/ra-core/src/routing/useGetPathForRecord.ts b/packages/ra-core/src/routing/useGetPathForRecord.ts index 7d1fa19d77a..4a34aa559d0 100644 --- a/packages/ra-core/src/routing/useGetPathForRecord.ts +++ b/packages/ra-core/src/routing/useGetPathForRecord.ts @@ -81,6 +81,11 @@ export const useGetPathForRecord = ( useEffect(() => { if (!record) return; + if (link === false) { + setPath(false); + return; + } + // Handle the inferred link type case if (link == null) { // We must check whether the resource has an edit view because if there is no diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx index 04734497c80..37bb980fa6c 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx @@ -6,13 +6,20 @@ import { waitFor, within, } from '@testing-library/react'; -import { ListContext, ResourceContextProvider } from 'ra-core'; +import { + ListContext, + ResourceContextProvider, + ResourceDefinitionContextProvider, +} from 'ra-core'; +import { Location } from 'react-router'; import { AdminContext } from '../../AdminContext'; import { SimpleList } from './SimpleList'; import { TextField } from '../../field/TextField'; import { + LinkType, NoPrimaryText, + RowClick, Standalone, StandaloneEmpty, } from './SimpleList.stories'; @@ -20,9 +27,20 @@ import { Basic } from '../filter/FilterButton.stories'; const Wrapper = ({ children }: any) => ( - - {children} - + + + {children} + + ); @@ -59,58 +77,24 @@ describe('', () => { }); it.each([ - [ - 'edit', - 'edit', - ['http://localhost/#/posts/1', 'http://localhost/#/posts/2'], - ], - [ - 'show', - 'show', - [ - 'http://localhost/#/posts/1/show', - 'http://localhost/#/posts/2/show', - ], - ], - [ - 'custom', - (record, id) => `/posts/${id}/custom`, - [ - 'http://localhost/#/posts/1/custom', - 'http://localhost/#/posts/2/custom', - ], - ], + ['edit', 'edit', '/books/1'], + ['show', 'show', '/books/1/show'], + ['custom', (record, id) => `/books/${id}/custom`, '/books/1/custom'], ])( - 'should render %s links for each item', - async (_, link, expectedUrls) => { + 'should render %s links for each item with linkType', + async (_, linkType, expectedUrls) => { + let location: Location; render( - { + location = l; }} - > - record.id.toString()} - secondaryText={} - /> - , - { wrapper: Wrapper } + /> ); - + fireEvent.click(await screen.findByText('War and Peace')); await waitFor(() => { - expect(screen.getByText('1').closest('a').href).toEqual( - expectedUrls[0] - ); - expect(screen.getByText('2').closest('a').href).toEqual( - expectedUrls[1] - ); + expect(location?.pathname).toEqual(expectedUrls); }); } ); @@ -143,6 +127,57 @@ describe('', () => { }); }); + it.each([ + ['edit', 'edit', '/books/1'], + ['show', 'show', '/books/1/show'], + ['custom', id => `/books/${id}/custom`, '/books/1/custom'], + ])( + 'should render %s links for each item with rowClick', + async (_, rowClick, expectedUrls) => { + let location: Location; + render( + { + location = l; + }} + /> + ); + fireEvent.click(await screen.findByText('War and Peace')); + await waitFor(() => { + expect(location?.pathname).toEqual(expectedUrls); + }); + } + ); + + it('should not render links if rowClick is false', async () => { + render( + + record.id.toString()} + secondaryText={} + /> + , + { wrapper: Wrapper } + ); + + await waitFor(() => { + expect(screen.getByText('1').closest('a')).toBeNull(); + expect(screen.getByText('2').closest('a')).toBeNull(); + }); + }); + it('should display a message when there is no result', () => { render( ', () => { }); it('should display a message when there is no result', async () => { render(); - await screen.findByText('No results found.'); + await screen.findByText('ra.navigation.no_results'); }); }); }); diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx index 91386aae39e..2ff72ddad5a 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.stories.tsx @@ -7,16 +7,19 @@ import { TestMemoryRouter, ResourceContextProvider, ResourceProps, + ResourceDefinitionContextProvider, } from 'ra-core'; import defaultMessages from 'ra-language-english'; import polyglotI18nProvider from 'ra-i18n-polyglot'; -import { Box, FormControlLabel, FormGroup, Switch } from '@mui/material'; +import { Alert, Box, FormControlLabel, FormGroup, Switch } from '@mui/material'; +import { Location } from 'react-router'; -import { SimpleList } from './SimpleList'; +import { FunctionLinkType, SimpleList } from './SimpleList'; import { AdminUI } from '../../AdminUI'; import { AdminContext, AdminContextProps } from '../../AdminContext'; import { EditGuesser } from '../../detail'; import { List, ListProps } from '../List'; +import { RowClickFunction } from '../types'; export default { title: 'ra-ui-materialui/list/SimpleList' }; @@ -105,17 +108,121 @@ const data = { export const Basic = () => ( - - record.title} - secondaryText={record => record.author} - tertiaryText={record => record.year} - /> - + + + record.title} + secondaryText={record => record.author} + tertiaryText={record => record.year} + /> + + ); +export const LinkType = ({ + linkType, + locationCallback, +}: { + linkType: string | FunctionLinkType | false; + locationCallback?: (l: Location) => void; +}) => ( + + + + + Inferred should target edit + record.title} + secondaryText={record => record.author} + tertiaryText={record => record.year} + linkType={linkType} + /> + + + + +); + +LinkType.args = { + linkType: 'edit', +}; +LinkType.argTypes = { + linkType: { + options: ['inferred', 'edit', 'show', 'no-link', 'function'], + mapping: { + inferred: undefined, + show: 'show', + edit: 'edit', + 'no-link': false, + function: (record, id) => alert(`Clicked on ${id}`), + }, + control: { type: 'select' }, + }, +}; + +export const RowClick = ({ + locationCallback, + rowClick, +}: { + locationCallback?: (l: Location) => void; + rowClick: string | RowClickFunction | false; +}) => ( + + + + + Inferred should target edit + record.title} + secondaryText={record => record.author} + tertiaryText={record => record.year} + rowClick={rowClick} + /> + + + + +); + +RowClick.args = { + rowClick: 'edit', +}; +RowClick.argTypes = { + rowClick: { + options: ['inferred', 'edit', 'show', 'no-link', 'function'], + mapping: { + inferred: undefined, + show: 'show', + edit: 'edit', + 'no-link': false, + function: id => alert(`Clicked on ${id}`), + }, + control: { type: 'select' }, + }, +}; + const myDataProvider = fakeRestDataProvider(data); const Wrapper = ({ @@ -289,26 +396,32 @@ export const FullAppInError = () => ( export const Standalone = () => ( - record.title} - secondaryText={record => record.author} - tertiaryText={record => record.year} - linkType={false} - /> + + + record.title} + secondaryText={record => record.author} + tertiaryText={record => record.year} + linkType={false} + /> + + ); export const StandaloneEmpty = () => ( - - - data={[]} - primaryText={record => record.title} - secondaryText={record => record.author} - tertiaryText={record => record.year} - linkType={false} - /> - + + + + data={[]} + primaryText={record => record.title} + secondaryText={record => record.author} + tertiaryText={record => record.year} + linkType={false} + /> + + ); diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx index eb2fe10364f..165a3afeef5 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx @@ -1,11 +1,7 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; import type { SxProps } from '@mui/material'; -import { isValidElement, ReactNode, ReactElement } from 'react'; import { Avatar, List, - ListProps, ListItem, ListItemAvatar, ListItemButton, @@ -13,22 +9,31 @@ import { ListItemProps, ListItemSecondaryAction, ListItemText, + ListProps, } from '@mui/material'; -import { Link } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; import { Identifier, + LinkToType, RaRecord, RecordContextProvider, sanitizeListRestProps, + useEvent, + useGetPathForRecord, + useGetPathForRecordCallback, + useGetRecordRepresentation, useListContextWithProps, + useRecordContext, useResourceContext, - useGetRecordRepresentation, - useCreatePath, useTranslate, } from 'ra-core'; +import * as React from 'react'; +import { isValidElement, ReactElement, ReactNode } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; -import { SimpleListLoading } from './SimpleListLoading'; import { ListNoResults } from '../ListNoResults'; +import { SimpleListLoading } from './SimpleListLoading'; +import { RowClickFunction } from '../types'; /** * The component renders a list of records as a Material UI . @@ -44,7 +49,8 @@ import { ListNoResults } from '../ListNoResults'; * - leftIcon: same * - rightAvatar: same * - rightIcon: same - * - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record + * - linkType: deprecated 'edit' or 'show', or a function returning 'edit' or 'show' based on the record + * - rowClick: The action to trigger when the user clicks on a row. * - rowStyle: function returning a style object based on (record, index) * - rowSx: function returning a sx object based on (record, index) * @@ -74,21 +80,20 @@ export const SimpleList = ( hasBulkActions, leftAvatar, leftIcon, - linkType = 'edit', + linkType, + rowClick, primaryText, rightAvatar, rightIcon, secondaryText, tertiaryText, + ref, rowSx, rowStyle, ...rest } = props; const { data, isPending, total } = useListContextWithProps(props); - const resource = useResourceContext(props); - const getRecordRepresentation = useGetRecordRepresentation(resource); - const translate = useTranslate(); if (isPending === true) { return ( @@ -102,21 +107,6 @@ export const SimpleList = ( ); } - const renderAvatar = ( - record: RecordType, - avatarCallback: FunctionToElement - ) => { - const avatarValue = avatarCallback(record, record.id); - if ( - typeof avatarValue === 'string' && - (avatarValue.startsWith('http') || avatarValue.startsWith('data:')) - ) { - return ; - } else { - return {avatarValue}; - } - }; - if (data == null || data.length === 0 || total === 0) { if (empty) { return empty; @@ -129,114 +119,21 @@ export const SimpleList = ( {data.map((record, rowIndex) => ( - - - {leftIcon && ( - - {leftIcon(record, record.id)} - - )} - {leftAvatar && ( - - {renderAvatar(record, leftAvatar)} - - )} - - {primaryText - ? typeof primaryText === 'string' - ? translate(primaryText, { - ...record, - _: primaryText, - }) - : isValidElement(primaryText) - ? primaryText - : // @ts-ignore - primaryText( - record, - record.id - ) - : getRecordRepresentation(record)} - - {!!tertiaryText && - (isValidElement(tertiaryText) ? ( - tertiaryText - ) : ( - - {typeof tertiaryText === - 'string' - ? translate( - tertiaryText, - { - ...record, - _: tertiaryText, - } - ) - : isValidElement( - tertiaryText - ) - ? tertiaryText - : // @ts-ignore - tertiaryText( - record, - record.id - )} - - ))} - - } - secondary={ - !!secondaryText && - (typeof secondaryText === 'string' - ? translate(secondaryText, { - ...record, - _: secondaryText, - }) - : isValidElement(secondaryText) - ? secondaryText - : // @ts-ignore - secondaryText(record, record.id)) - } - /> - {(rightAvatar || rightIcon) && ( - - {rightAvatar && ( - - {renderAvatar(record, rightAvatar)} - - )} - {rightIcon && ( - - {rightIcon(record, record.id)} - - )} - - )} - - + ))} @@ -248,21 +145,45 @@ export type FunctionToElement = ( id: Identifier ) => ReactNode; -export interface SimpleListProps - extends Omit { - className?: string; - empty?: ReactElement; - hasBulkActions?: boolean; +interface SimpleListBaseProps { leftAvatar?: FunctionToElement; leftIcon?: FunctionToElement; primaryText?: FunctionToElement | ReactElement | string; + /** + * @deprecated use rowClick instead + */ linkType?: string | FunctionLinkType | false; + + /** + * The action to trigger when the user clicks on a row. + * + * @see https://marmelab.com/react-admin/Datagrid.html#rowclick + * @example + * import { List, Datagrid } from 'react-admin'; + * + * export const PostList = () => ( + * + * + * ... + * + + * + * ); + */ + rowClick?: string | RowClickFunction | false; rightAvatar?: FunctionToElement; rightIcon?: FunctionToElement; secondaryText?: FunctionToElement | ReactElement | string; tertiaryText?: FunctionToElement | ReactElement | string; rowSx?: (record: RecordType, index: number) => SxProps; rowStyle?: (record: RecordType, index: number) => any; +} +export interface SimpleListProps + extends SimpleListBaseProps, + Omit { + className?: string; + empty?: ReactElement; + hasBulkActions?: boolean; // can be injected when using the component without context resource?: string; data?: RecordType[]; @@ -272,41 +193,225 @@ export interface SimpleListProps total?: number; } -const LinkOrNot = ( - props: LinkOrNotProps & Omit +const SimpleListItem = ( + props: SimpleListItemProps ) => { - const { - classes: classesOverride, - linkType, - resource, - id, - children, - record, - ...rest - } = props; - const createPath = useCreatePath(); - const type = - typeof linkType === 'function' ? linkType(record, id) : linkType; + const { linkType, rowClick, rowIndex, rowSx, rowStyle } = props; + const resource = useResourceContext(props); + const record = useRecordContext(props); + const navigate = useNavigate(); + // If we don't have a function to get the path, we can compute the path immediately and set the href + // on the Link correctly without onClick (better for accessibility) + const isFunctionLink = + typeof linkType === 'function' || typeof rowClick === 'function'; + const pathForRecord = useGetPathForRecord({ + link: isFunctionLink ? false : linkType ?? rowClick, + }); + const getPathForRecord = useGetPathForRecordCallback(); + const handleClick = useEvent( + async (event: React.MouseEvent) => { + // No need to handle non function linkType or rowClick + if (!isFunctionLink) return; + if (!record) return; + event.persist(); - if (type === false) { - return {children}; + let link: LinkToType = + typeof linkType === 'function' + ? linkType(record, record.id) + : typeof rowClick === 'function' + ? (record, resource) => + rowClick(record.id, resource, record) + : false; + + const path = await getPathForRecord({ + record, + resource, + link, + }); + if (path === false || path == null) { + return; + } + navigate(path); + } + ); + + if (!record) return null; + + if (isFunctionLink) { + return ( + + {/* @ts-ignore */} + + + + + ); } + + if (pathForRecord) { + return ( + + + + + + ); + } + return ( - // @ts-ignore - - {children} - + + ); }; +const SimpleListItemContent = ( + props: SimpleListItemProps +) => { + const { + leftAvatar, + leftIcon, + primaryText, + rightAvatar, + rightIcon, + secondaryText, + tertiaryText, + } = props; + const resource = useResourceContext(props); + const record = useRecordContext(props); + const getRecordRepresentation = useGetRecordRepresentation(resource); + const translate = useTranslate(); + + const renderAvatar = ( + record: RecordType, + avatarCallback: FunctionToElement + ) => { + const avatarValue = avatarCallback(record, record.id); + if ( + typeof avatarValue === 'string' && + (avatarValue.startsWith('http') || avatarValue.startsWith('data:')) + ) { + return ; + } else { + return {avatarValue}; + } + }; + + if (!record) return null; + + return ( + <> + {leftIcon && ( + {leftIcon(record, record.id)} + )} + {leftAvatar && ( + + {renderAvatar(record, leftAvatar)} + + )} + + {primaryText + ? typeof primaryText === 'string' + ? translate(primaryText, { + ...record, + _: primaryText, + }) + : isValidElement(primaryText) + ? primaryText + : // @ts-ignore + primaryText(record, record.id) + : getRecordRepresentation(record)} + + {!!tertiaryText && + (isValidElement(tertiaryText) ? ( + tertiaryText + ) : ( + + {typeof tertiaryText === 'string' + ? translate(tertiaryText, { + ...record, + _: tertiaryText, + }) + : isValidElement(tertiaryText) + ? tertiaryText + : // @ts-ignore + tertiaryText(record, record.id)} + + ))} + + } + secondary={ + !!secondaryText && + (typeof secondaryText === 'string' + ? translate(secondaryText, { + ...record, + _: secondaryText, + }) + : isValidElement(secondaryText) + ? secondaryText + : // @ts-ignore + secondaryText(record, record.id)) + } + /> + {(rightAvatar || rightIcon) && ( + + {rightAvatar && ( + {renderAvatar(record, rightAvatar)} + )} + {rightIcon && ( + + {rightIcon(record, record.id)} + + )} + + )} + + ); +}; + +interface SimpleListItemProps + extends SimpleListBaseProps, + Omit { + rowIndex: number; +} + export type FunctionLinkType = (record: RaRecord, id: Identifier) => string; export interface LinkOrNotProps { - linkType: string | FunctionLinkType | false; + // @deprecated: use rowClick instead + linkType?: string | FunctionLinkType | false; + rowClick?: string | RowClickFunction | false; resource?: string; id: Identifier; record: RaRecord; diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 9a373ad9ba6..48ed65064c7 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -29,7 +29,7 @@ import difference from 'lodash/difference'; import { DatagridHeader } from './DatagridHeader'; import DatagridLoading from './DatagridLoading'; import DatagridBody, { PureDatagridBody } from './DatagridBody'; -import { RowClickFunction } from './DatagridRow'; +import { RowClickFunction } from '../types'; import DatagridContextProvider from './DatagridContextProvider'; import { DatagridClasses, DatagridRoot } from './useDatagridStyles'; import { BulkActionsToolbar } from '../BulkActionsToolbar'; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx index 84c68e6f020..1504d05c348 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridBody.tsx @@ -4,8 +4,9 @@ import { SxProps, TableBody, TableBodyProps } from '@mui/material'; import clsx from 'clsx'; import { Identifier, RaRecord, RecordContextProvider } from 'ra-core'; +import { RowClickFunction } from '../types'; import { DatagridClasses } from './useDatagridStyles'; -import DatagridRow, { PureDatagridRow, RowClickFunction } from './DatagridRow'; +import DatagridRow, { PureDatagridRow } from './DatagridRow'; const DatagridBody: React.ForwardRefExoticComponent< Omit & diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx index 1197027ed18..dac125aad37 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx @@ -27,6 +27,7 @@ import DatagridCell from './DatagridCell'; import ExpandRowButton from './ExpandRowButton'; import { DatagridClasses } from './useDatagridStyles'; import { useDatagridContext } from './useDatagridContext'; +import { RowClickFunction } from '../types'; const computeNbColumns = (expand, children, hasBulkActions) => expand @@ -265,12 +266,6 @@ export interface DatagridRowProps selectable?: boolean; } -export type RowClickFunction = ( - id: Identifier, - resource: string, - record: RaRecord -) => string | false | Promise; - const areEqual = (prevProps, nextProps) => { const { children: _1, expand: _2, ...prevPropsWithoutChildren } = prevProps; const { children: _3, expand: _4, ...nextPropsWithoutChildren } = nextProps; diff --git a/packages/ra-ui-materialui/src/list/datagrid/index.ts b/packages/ra-ui-materialui/src/list/datagrid/index.ts index 6c343b05785..4691d44d4ed 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/index.ts +++ b/packages/ra-ui-materialui/src/list/datagrid/index.ts @@ -8,11 +8,7 @@ import DatagridHeaderCell, { DatagridHeaderCellProps, } from './DatagridHeaderCell'; import DatagridLoading, { DatagridLoadingProps } from './DatagridLoading'; -import DatagridRow, { - DatagridRowProps, - PureDatagridRow, - RowClickFunction, -} from './DatagridRow'; +import DatagridRow, { DatagridRowProps, PureDatagridRow } from './DatagridRow'; import ExpandRowButton, { ExpandRowButtonProps } from './ExpandRowButton'; export * from './Datagrid'; @@ -43,5 +39,4 @@ export type { DatagridLoadingProps, DatagridRowProps, ExpandRowButtonProps, - RowClickFunction, }; diff --git a/packages/ra-ui-materialui/src/list/index.ts b/packages/ra-ui-materialui/src/list/index.ts index 4f66d57ec52..3b826932002 100644 --- a/packages/ra-ui-materialui/src/list/index.ts +++ b/packages/ra-ui-materialui/src/list/index.ts @@ -16,3 +16,4 @@ export * from './pagination'; export * from './Placeholder'; export * from './SimpleList'; export * from './SingleFieldList'; +export * from './types'; diff --git a/packages/ra-ui-materialui/src/list/types.ts b/packages/ra-ui-materialui/src/list/types.ts new file mode 100644 index 00000000000..73b472b17fa --- /dev/null +++ b/packages/ra-ui-materialui/src/list/types.ts @@ -0,0 +1,7 @@ +import { Identifier, RaRecord } from 'ra-core'; + +export type RowClickFunction = ( + id: Identifier, + resource: string, + record: RecordType +) => string | false | Promise; From ffbac39d867a9fdf7043b563e9522d73f6d64eab Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:26:56 +0100 Subject: [PATCH 03/33] Extract SimpleListItem --- .../src/list/SimpleList/SimpleList.tsx | 196 ++---------------- .../src/list/SimpleList/SimpleListItem.tsx | 164 +++++++++++++++ 2 files changed, 183 insertions(+), 177 deletions(-) create mode 100644 packages/ra-ui-materialui/src/list/SimpleList/SimpleListItem.tsx diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx index 165a3afeef5..76efa55943c 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx @@ -1,26 +1,17 @@ -import type { SxProps } from '@mui/material'; import { Avatar, List, - ListItem, ListItemAvatar, - ListItemButton, ListItemIcon, - ListItemProps, ListItemSecondaryAction, ListItemText, ListProps, } from '@mui/material'; import { styled } from '@mui/material/styles'; import { - Identifier, - LinkToType, RaRecord, RecordContextProvider, sanitizeListRestProps, - useEvent, - useGetPathForRecord, - useGetPathForRecordCallback, useGetRecordRepresentation, useListContextWithProps, useRecordContext, @@ -28,12 +19,16 @@ import { useTranslate, } from 'ra-core'; import * as React from 'react'; -import { isValidElement, ReactElement, ReactNode } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { isValidElement, ReactElement } from 'react'; import { ListNoResults } from '../ListNoResults'; import { SimpleListLoading } from './SimpleListLoading'; -import { RowClickFunction } from '../types'; +import { + FunctionToElement, + SimpleListBaseProps, + SimpleListItem, + SimpleListItemProps, +} from './SimpleListItem'; /** * The component renders a list of records as a Material UI . @@ -122,62 +117,28 @@ export const SimpleList = ( + > + + ))} ); }; -export type FunctionToElement = ( - record: RecordType, - id: Identifier -) => ReactNode; - -interface SimpleListBaseProps { - leftAvatar?: FunctionToElement; - leftIcon?: FunctionToElement; - primaryText?: FunctionToElement | ReactElement | string; - /** - * @deprecated use rowClick instead - */ - linkType?: string | FunctionLinkType | false; - - /** - * The action to trigger when the user clicks on a row. - * - * @see https://marmelab.com/react-admin/Datagrid.html#rowclick - * @example - * import { List, Datagrid } from 'react-admin'; - * - * export const PostList = () => ( - * - * - * ... - * - - * - * ); - */ - rowClick?: string | RowClickFunction | false; - rightAvatar?: FunctionToElement; - rightIcon?: FunctionToElement; - secondaryText?: FunctionToElement | ReactElement | string; - tertiaryText?: FunctionToElement | ReactElement | string; - rowSx?: (record: RecordType, index: number) => SxProps; - rowStyle?: (record: RecordType, index: number) => any; -} export interface SimpleListProps extends SimpleListBaseProps, Omit { @@ -193,107 +154,6 @@ export interface SimpleListProps total?: number; } -const SimpleListItem = ( - props: SimpleListItemProps -) => { - const { linkType, rowClick, rowIndex, rowSx, rowStyle } = props; - const resource = useResourceContext(props); - const record = useRecordContext(props); - const navigate = useNavigate(); - // If we don't have a function to get the path, we can compute the path immediately and set the href - // on the Link correctly without onClick (better for accessibility) - const isFunctionLink = - typeof linkType === 'function' || typeof rowClick === 'function'; - const pathForRecord = useGetPathForRecord({ - link: isFunctionLink ? false : linkType ?? rowClick, - }); - const getPathForRecord = useGetPathForRecordCallback(); - const handleClick = useEvent( - async (event: React.MouseEvent) => { - // No need to handle non function linkType or rowClick - if (!isFunctionLink) return; - if (!record) return; - event.persist(); - - let link: LinkToType = - typeof linkType === 'function' - ? linkType(record, record.id) - : typeof rowClick === 'function' - ? (record, resource) => - rowClick(record.id, resource, record) - : false; - - const path = await getPathForRecord({ - record, - resource, - link, - }); - if (path === false || path == null) { - return; - } - navigate(path); - } - ); - - if (!record) return null; - - if (isFunctionLink) { - return ( - - {/* @ts-ignore */} - - - - - ); - } - - if (pathForRecord) { - return ( - - - - - - ); - } - - return ( - - - - ); -}; - const SimpleListItemContent = ( props: SimpleListItemProps ) => { @@ -400,24 +260,6 @@ const SimpleListItemContent = ( ); }; -interface SimpleListItemProps - extends SimpleListBaseProps, - Omit { - rowIndex: number; -} - -export type FunctionLinkType = (record: RaRecord, id: Identifier) => string; - -export interface LinkOrNotProps { - // @deprecated: use rowClick instead - linkType?: string | FunctionLinkType | false; - rowClick?: string | RowClickFunction | false; - resource?: string; - id: Identifier; - record: RaRecord; - children: ReactNode; -} - const PREFIX = 'RaSimpleList'; export const SimpleListClasses = { diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleListItem.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListItem.tsx new file mode 100644 index 00000000000..a05ffe65be9 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleListItem.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import { ReactElement, ReactNode } from 'react'; +import type { SxProps } from '@mui/material'; +import { ListItem, ListItemButton, ListItemProps } from '@mui/material'; +import { + Identifier, + LinkToType, + RaRecord, + useEvent, + useGetPathForRecord, + useGetPathForRecordCallback, + useRecordContext, + useResourceContext, +} from 'ra-core'; +import { Link, useNavigate } from 'react-router-dom'; +import { RowClickFunction } from '../types'; + +export const SimpleListItem = ( + props: SimpleListItemProps +) => { + const { children, linkType, rowClick, rowIndex, rowSx, rowStyle } = props; + const resource = useResourceContext(props); + const record = useRecordContext(props); + const navigate = useNavigate(); + // If we don't have a function to get the path, we can compute the path immediately and set the href + // on the Link correctly without onClick (better for accessibility) + const isFunctionLink = + typeof linkType === 'function' || typeof rowClick === 'function'; + const pathForRecord = useGetPathForRecord({ + link: isFunctionLink ? false : linkType ?? rowClick, + }); + const getPathForRecord = useGetPathForRecordCallback(); + const handleClick = useEvent( + async (event: React.MouseEvent) => { + // No need to handle non function linkType or rowClick + if (!isFunctionLink) return; + if (!record) return; + event.persist(); + + let link: LinkToType = + typeof linkType === 'function' + ? linkType(record, record.id) + : typeof rowClick === 'function' + ? (record, resource) => + rowClick(record.id, resource, record) + : false; + + const path = await getPathForRecord({ + record, + resource, + link, + }); + if (path === false || path == null) { + return; + } + navigate(path); + } + ); + + if (!record) return null; + + if (isFunctionLink) { + return ( + + {/* @ts-ignore */} + + {children} + + + ); + } + + if (pathForRecord) { + return ( + + + {children} + + + ); + } + + return ( + + {children} + + ); +}; + +export type FunctionToElement = ( + record: RecordType, + id: Identifier +) => ReactNode; + +export type FunctionLinkType = (record: RaRecord, id: Identifier) => string; + +export interface SimpleListBaseProps { + leftAvatar?: FunctionToElement; + leftIcon?: FunctionToElement; + primaryText?: FunctionToElement | ReactElement | string; + /** + * @deprecated use rowClick instead + */ + linkType?: string | FunctionLinkType | false; + + /** + * The action to trigger when the user clicks on a row. + * + * @see https://marmelab.com/react-admin/Datagrid.html#rowclick + * @example + * import { List, Datagrid } from 'react-admin'; + * + * export const PostList = () => ( + * + * + * ... + * + + * + * ); + */ + rowClick?: string | RowClickFunction | false; + rightAvatar?: FunctionToElement; + rightIcon?: FunctionToElement; + secondaryText?: FunctionToElement | ReactElement | string; + tertiaryText?: FunctionToElement | ReactElement | string; + rowSx?: (record: RecordType, index: number) => SxProps; + rowStyle?: (record: RecordType, index: number) => any; +} + +export interface SimpleListItemProps + extends SimpleListBaseProps, + Omit { + rowIndex: number; +} From 2c821b23aa415ab1ba895efd369e767edd68a9ad Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 27 Nov 2024 19:06:02 +0100 Subject: [PATCH 04/33] Add unit tests --- .../src/input/TextArrayInput.spec.tsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx diff --git a/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx new file mode 100644 index 00000000000..6622e3e8322 --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; + +import { Basic, HelperText, Label, Required } from './TextArrayInput.stories'; + +describe('', () => { + it('should render the values as chips', () => { + render(); + const chip1 = screen.getByText('john@example.com'); + expect(chip1.classList.contains('MuiChip-label')).toBe(true); + const chip2 = screen.getByText('albert@target.dev'); + expect(chip2.classList.contains('MuiChip-label')).toBe(true); + }); + it('should allow to remove a value', async () => { + render(); + await screen.findByText( + '["john@example.com","albert@target.dev"] (object)' + ); + const deleteButtons = screen.getAllByTestId('CancelIcon'); + fireEvent.click(deleteButtons[0]); + await screen.findByText('["albert@target.dev"] (object)'); + }); + it('should allow to remove all values one by one', async () => { + render(); + await screen.findByText( + '["john@example.com","albert@target.dev"] (object)' + ); + const deleteButtons = screen.getAllByTestId('CancelIcon'); + fireEvent.click(deleteButtons[1]); + fireEvent.click(deleteButtons[0]); + await screen.findByText('[] (object)'); + }); + it('should allow to remove all values using the reset button', async () => { + render(); + const input = screen.getByLabelText('resources.emails.fields.to'); + fireEvent.click(input); + const clearButton = screen.getByLabelText('Clear'); + fireEvent.click(clearButton); + await screen.findByText('[] (object)'); + }); + it('should allow to add a value', async () => { + render(); + const input = screen.getByLabelText('resources.emails.fields.to'); + fireEvent.change(input, { target: { value: 'bob.brown@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + await screen.findByText( + '["john@example.com","albert@target.dev","bob.brown@example.com"] (object)' + ); + }); + it('should render the helper text', () => { + render(); + screen.getByText('Email addresses of the recipients'); + }); + it('should render the custom label', () => { + render( )} show={ShowGuesser} + edit={() => ( + + + + + + + + )} create={() => ( Date: Thu, 5 Dec 2024 14:46:54 +0100 Subject: [PATCH 12/33] Update examples/simple/src/data.tsx --- examples/simple/src/data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple/src/data.tsx b/examples/simple/src/data.tsx index 69a0ac1fd47..f1276b78534 100644 --- a/examples/simple/src/data.tsx +++ b/examples/simple/src/data.tsx @@ -1,7 +1,7 @@ export default { posts: [ { - id: '1 1', + id: 1, title: 'Accusantium qui nihil voluptatum quia voluptas maxime ab similique', teaser: 'In facilis aut aut odit hic doloribus. Fugit possimus perspiciatis sit molestias in. Sunt dignissimos sed quis at vitae veniam amet. Sint sunt perspiciatis quis doloribus aperiam numquam consequatur et. Blanditiis aut earum incidunt eos magnam et voluptatem. Minima iure voluptatum autem. At eaque sit aperiam minima aut in illum.', body: '

    Rerum velit quos est similique. Consectetur tempora eos ullam velit nobis sit debitis. Magni explicabo omnis delectus labore vel recusandae.

    Aut a minus laboriosam harum placeat quas minima fuga. Quos nulla fuga quam officia tempore. Rerum occaecati ut eum et tempore. Nam ab repudiandae et nemo praesentium.

    Cumque corporis officia occaecati ducimus sequi laborum omnis ut. Nam aspernatur veniam fugit. Nihil eum libero ea dolorum ducimus impedit sed. Quidem inventore porro corporis debitis eum in. Nesciunt unde est est qui nulla. Esse sunt placeat molestiae molestiae sed quia. Sunt qui quidem quos velit reprehenderit quos blanditiis ducimus. Sint et molestiae maxime ut consequatur minima. Quaerat rem voluptates voluptatem quos. Corporis perferendis in provident iure. Commodi odit exercitationem excepturi et deserunt qui.

    Optio iste necessitatibus velit non. Neque sed occaecati culpa porro culpa. Quia quam in molestias ratione et necessitatibus consequatur. Est est tempora consequatur voluptatem vel. Mollitia tenetur non quis omnis perspiciatis deserunt sed necessitatibus. Ad rerum reiciendis sunt aspernatur.

    Est ullam ut magni aspernatur. Eum et sed tempore modi.

    Earum aperiam sit neque quo laborum suscipit unde. Expedita nostrum itaque non non adipisci. Ut delectus quis delectus est at sint. Iste hic qui ea eaque eaque sed id. Hic placeat rerum numquam id velit deleniti voluptatem. Illum adipisci voluptas adipisci ut alias. Earum exercitationem iste quidem eveniet aliquid hic reiciendis. Exercitationem est sunt in minima consequuntur. Aut quaerat libero dolorem.

    ', From bdff5d3e9576fa81bb0ff711a9519e7299046d10 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 10 Dec 2024 17:06:03 +0100 Subject: [PATCH 13/33] Fix community youtube link --- docs/Community.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Community.md b/docs/Community.md index 620a5f9d087..77db6beaf85 100644 --- a/docs/Community.md +++ b/docs/Community.md @@ -45,7 +45,7 @@ On our [Youtube channel](https://www.youtube.com/@react-admin), you can find som
    From d8d26470d8c1715a04b66955aac536e882c6acb9 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 10 Dec 2024 17:11:25 +0100 Subject: [PATCH 14/33] [Doc] Add Link to BlueSky account --- docs/Community.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Community.md b/docs/Community.md index 77db6beaf85..ecbd8366fe9 100644 --- a/docs/Community.md +++ b/docs/Community.md @@ -49,6 +49,12 @@ On our [Youtube channel](https://www.youtube.com/@react-admin), you can find som +## Bluesky + +Follow us on BlueSky to get the latest news about react-admin: + +

    Hi Bluesky community👋React-admin is an open-source framework for building B2B apps.🚀Backed by the team at @marmelab.bsky.social, we’re committed to empowering developers to build faster & smarter.📲 Follow this account to stay in the loop on all things react-admin.marmelab.com/react-admin/

    [image or embed]

    — react-admin (@react-admin.bsky.social) November 22, 2024 at 11:23 AM
    + ## Support If you're stuck with a problem in your react-admin code, you can get help from various channels: From 66845e75cb73471b27b1634926a22fd7ad759d5e Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 10 Dec 2024 17:11:58 +0100 Subject: [PATCH 15/33] Revert "[Doc] Add Link to BlueSky account" This reverts commit d8d26470d8c1715a04b66955aac536e882c6acb9. --- docs/Community.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/Community.md b/docs/Community.md index ecbd8366fe9..77db6beaf85 100644 --- a/docs/Community.md +++ b/docs/Community.md @@ -49,12 +49,6 @@ On our [Youtube channel](https://www.youtube.com/@react-admin), you can find som -## Bluesky - -Follow us on BlueSky to get the latest news about react-admin: - -

    Hi Bluesky community👋React-admin is an open-source framework for building B2B apps.🚀Backed by the team at @marmelab.bsky.social, we’re committed to empowering developers to build faster & smarter.📲 Follow this account to stay in the loop on all things react-admin.marmelab.com/react-admin/

    [image or embed]

    — react-admin (@react-admin.bsky.social) November 22, 2024 at 11:23 AM
    - ## Support If you're stuck with a problem in your react-admin code, you can get help from various channels: From 33657d10b509947fe5489e5fb52740144617bdb9 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 10 Dec 2024 17:18:01 +0100 Subject: [PATCH 16/33] [Doc] Fix props order in SimpleList doc --- docs/SimpleList.md | 54 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/SimpleList.md b/docs/SimpleList.md index b63a5d1c8ac..98379633bde 100644 --- a/docs/SimpleList.md +++ b/docs/SimpleList.md @@ -80,33 +80,6 @@ This prop should be a function returning an `` component. When present, This prop should be a function returning an `` component. When present, the `` renders a `` before the `` -## `rowClick` - -The `` items link to the edition page by default. You can also set the `rowClick` prop to `show` directly to link to the `` page instead. - -```jsx -import { List, SimpleList } from 'react-admin'; - -export const PostList = () => ( - - record.title} - secondaryText={record => `${record.views} views`} - tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - rowClick="show" - /> - -); -``` - -`rowClick` accepts the following values: - -* `rowClick="edit"`: links to the edit page. This is the default behavior. -* `rowClick="show"`: links to the show page. -* `rowClick={false}`: does not link to anything. -* `rowClick="/custom"`: links to a custom path. -* `rowClick={(id, resource, record) => path}`: path can be any of the above values - ## `primaryText` The `primaryText`, `secondaryText` and `tertiaryText` props can accept 4 types of values: @@ -192,6 +165,33 @@ This prop should be a function returning an `` component. When present, This prop should be a function returning an `` component. When present, the `` renders a `` after the ``. +## `rowClick` + +The `` items link to the edition page by default. You can also set the `rowClick` prop to `show` directly to link to the `` page instead. + +```jsx +import { List, SimpleList } from 'react-admin'; + +export const PostList = () => ( + + record.title} + secondaryText={record => `${record.views} views`} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + rowClick="show" + /> + +); +``` + +`rowClick` accepts the following values: + +* `rowClick="edit"`: links to the edit page. This is the default behavior. +* `rowClick="show"`: links to the show page. +* `rowClick={false}`: does not link to anything. +* `rowClick="/custom"`: links to a custom path. +* `rowClick={(id, resource, record) => path}`: path can be any of the above values + ## `rowStyle` *Deprecated - use [`rowSx`](#rowsx) instead.* From b69dfe11697135196733bb9240012bb1c969d061 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:47:31 +0100 Subject: [PATCH 17/33] Extract getRecordFromLocation in a hook --- .../create/useCreateController.spec.tsx | 41 +--- .../controller/create/useCreateController.ts | 39 +--- .../src/core/SourceContext.stories.tsx | 21 +- .../src/form/FormDataConsumer.spec.tsx | 22 +- packages/ra-core/src/form/index.ts | 1 + .../src/form/useRecordFromLocation.spec.tsx | 188 ++++++++++++++++++ .../ra-core/src/form/useRecordFromLocation.ts | 71 +++++++ 7 files changed, 288 insertions(+), 95 deletions(-) create mode 100644 packages/ra-core/src/form/useRecordFromLocation.spec.tsx create mode 100644 packages/ra-core/src/form/useRecordFromLocation.ts diff --git a/packages/ra-core/src/controller/create/useCreateController.spec.tsx b/packages/ra-core/src/controller/create/useCreateController.spec.tsx index c51cb411062..b873e426384 100644 --- a/packages/ra-core/src/controller/create/useCreateController.spec.tsx +++ b/packages/ra-core/src/controller/create/useCreateController.spec.tsx @@ -7,7 +7,7 @@ import { } from '@testing-library/react'; import expect from 'expect'; import React from 'react'; -import { Location, Route, Routes } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { CreateContextProvider, @@ -26,50 +26,11 @@ import { useRegisterMutationMiddleware, } from '../saveContext'; import { CreateController } from './CreateController'; -import { getRecordFromLocation } from './useCreateController'; import { TestMemoryRouter } from '../../routing'; import { CanAccess } from './useCreateController.security.stories'; describe('useCreateController', () => { - describe('getRecordFromLocation', () => { - const location: Location = { - key: 'a_key', - pathname: '/foo', - search: '', - state: undefined, - hash: '', - }; - - it('should return location state record when set', () => { - expect( - getRecordFromLocation({ - ...location, - state: { record: { foo: 'bar' } }, - }) - ).toEqual({ foo: 'bar' }); - }); - - it('should return location search when set', () => { - expect( - getRecordFromLocation({ - ...location, - search: '?source={"foo":"baz","array":["1","2"]}', - }) - ).toEqual({ foo: 'baz', array: ['1', '2'] }); - }); - - it('should return location state record when both state and search are set', () => { - expect( - getRecordFromLocation({ - ...location, - state: { record: { foo: 'bar' } }, - search: '?foo=baz', - }) - ).toEqual({ foo: 'bar' }); - }); - }); - const defaultProps = { hasCreate: true, hasEdit: true, diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts index c07e6a469d4..5c1fff4e64e 100644 --- a/packages/ra-core/src/controller/create/useCreateController.ts +++ b/packages/ra-core/src/controller/create/useCreateController.ts @@ -1,6 +1,4 @@ import { useCallback } from 'react'; -import { parse } from 'query-string'; -import { useLocation, Location } from 'react-router-dom'; import { UseMutationOptions } from '@tanstack/react-query'; import { useAuthenticated, useRequireAccess } from '../../auth'; @@ -23,6 +21,7 @@ import { useResourceDefinition, useGetResourceLabel, } from '../../core'; +import { useRecordFromLocation } from '../../form'; /** * Prepare data for the Create view @@ -78,11 +77,10 @@ export const useCreateController = < const { hasEdit, hasShow } = useResourceDefinition(props); const finalRedirectTo = redirectTo ?? getDefaultRedirectRoute(hasShow, hasEdit); - const location = useLocation(); const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); - const recordToUse = record ?? getRecordFromLocation(location) ?? undefined; + const recordToUse = useRecordFromLocation({ record }); const { onSuccess, onError, meta, ...otherMutationOptions } = mutationOptions; const { @@ -239,39 +237,6 @@ export interface CreateControllerResult< saving: boolean; } -/** - * Get the initial record from the location, whether it comes from the location - * state or is serialized in the url search part. - */ -export const getRecordFromLocation = ({ state, search }: Location) => { - if (state && (state as StateWithRecord).record) { - return (state as StateWithRecord).record; - } - if (search) { - try { - const searchParams = parse(search); - if (searchParams.source) { - if (Array.isArray(searchParams.source)) { - console.error( - `Failed to parse location search parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified source parameter (e.g. '?source={"title":"foo"}')` - ); - return; - } - return JSON.parse(searchParams.source); - } - } catch (e) { - console.error( - `Failed to parse location search parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified source parameter (e.g. '?source={"title":"foo"}')` - ); - } - } - return null; -}; - -type StateWithRecord = { - record?: Partial; -}; - const getDefaultRedirectRoute = (hasShow, hasEdit) => { if (hasEdit) { return 'edit'; diff --git a/packages/ra-core/src/core/SourceContext.stories.tsx b/packages/ra-core/src/core/SourceContext.stories.tsx index d5654766b86..839398bc635 100644 --- a/packages/ra-core/src/core/SourceContext.stories.tsx +++ b/packages/ra-core/src/core/SourceContext.stories.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { Form, useInput } from '../form'; +import { TestMemoryRouter } from '..'; export default { title: 'ra-core/core/SourceContext', @@ -19,19 +20,23 @@ const TextInput = props => { export const Basic = () => { return ( -
    - - + +
    + + +
    ); }; export const WithoutSourceContext = () => { const form = useForm(); return ( - -
    - - -
    + + +
    + + +
    +
    ); }; diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 6bfb2fdcf11..d7ffd7497ca 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -12,7 +12,7 @@ import { ArrayInput, } from 'ra-ui-materialui'; import expect from 'expect'; -import { Form, ResourceContextProvider } from '..'; +import { Form, ResourceContextProvider, TestMemoryRouter } from '..'; describe('FormDataConsumerView', () => { it('does not call its children function with scopedFormData if it did not receive a source containing an index', () => { @@ -20,15 +20,17 @@ describe('FormDataConsumerView', () => { const formData = { id: 123, title: 'A title' }; render( -
    - - {children} - -
    + +
    + + {children} + +
    +
    ); expect(children).toHaveBeenCalledWith({ diff --git a/packages/ra-core/src/form/index.ts b/packages/ra-core/src/form/index.ts index 73e4ed0c12b..0210416560a 100644 --- a/packages/ra-core/src/form/index.ts +++ b/packages/ra-core/src/form/index.ts @@ -5,6 +5,7 @@ export * from './groups'; export * from './useApplyInputDefaultValues'; export * from './useAugmentedForm'; export * from './useInput'; +export * from './useRecordFromLocation'; export * from './useSuggestions'; export * from './useWarnWhenUnsavedChanges'; export * from './validation'; diff --git a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx new file mode 100644 index 00000000000..2dfa8992bd6 --- /dev/null +++ b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx @@ -0,0 +1,188 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { + getRecordFromLocation, + useRecordFromLocation, +} from './useRecordFromLocation'; +import { + RecordContextProvider, + TestMemoryRouter, + UseRecordFromLocationOptions, +} from '..'; + +describe('useRecordFromLocation', () => { + const UseGetRecordFromLocation = (props: UseRecordFromLocationOptions) => { + const recordFromLocation = useRecordFromLocation(props); + + return
    {JSON.stringify(recordFromLocation)}
    ; + }; + it('return the record from the location search', async () => { + const record = { test: 'value' }; + render( + + + + ); + + await screen.findByText(JSON.stringify(record)); + }); + it('return the record from the RecordContext as is if there is no location search nor state that contains a record', async () => { + render( + + + + + + ); + + await screen.findByText(JSON.stringify({ initial: 'initial value' })); + }); + it('return merge the record from the RecordContext with the one from the location search', async () => { + const record = { test: 'value' }; + render( + + + + + + ); + + await screen.findByText( + JSON.stringify({ initial: 'initial value', test: 'value' }) + ); + }); + it('return merge the record from the RecordContext with the one from the location state', async () => { + const record = { test: 'value' }; + render( + + + + + + ); + + await screen.findByText( + JSON.stringify({ initial: 'initial value', test: 'value' }) + ); + }); + it('return merge the record passed as option with the one from the location search', async () => { + const record = { test: 'value' }; + render( + + + + + + ); + + await screen.findByText( + JSON.stringify({ anotherRecord: 'another value', test: 'value' }) + ); + }); + it('return merge the record passed as option with the one from the location state', async () => { + const record = { test: 'value' }; + render( + + + + + + ); + + await screen.findByText( + JSON.stringify({ anotherRecord: 'another value', test: 'value' }) + ); + }); +}); + +describe('getRecordFromLocation', () => { + const location: Location = { + key: 'a_key', + pathname: '/foo', + search: '', + state: undefined, + hash: '', + }; + + it('should return location state record when set', () => { + expect( + getRecordFromLocation({ + location: { + ...location, + state: { record: { foo: 'bar' } }, + }, + }) + ).toEqual({ foo: 'bar' }); + }); + + it('should return location state record when set with a custom key', () => { + expect( + getRecordFromLocation({ + location: { + ...location, + state: { myRecord: { foo: 'bar' } }, + }, + stateSource: 'myRecord', + }) + ).toEqual({ foo: 'bar' }); + }); + + it('should return location search when set', () => { + expect( + getRecordFromLocation({ + location: { + ...location, + search: '?source={"foo":"baz","array":["1","2"]}', + }, + }) + ).toEqual({ foo: 'baz', array: ['1', '2'] }); + }); + + it('should return location search when set with a custom key', () => { + expect( + getRecordFromLocation({ + location: { + ...location, + search: '?mySource={"foo":"baz","array":["1","2"]}', + }, + searchSource: 'mySource', + }) + ).toEqual({ foo: 'baz', array: ['1', '2'] }); + }); + + it('should return location state record when both state and search are set', () => { + expect( + getRecordFromLocation({ + location: { + ...location, + state: { record: { foo: 'bar' } }, + search: '?foo=baz', + }, + }) + ).toEqual({ foo: 'bar' }); + }); +}); diff --git a/packages/ra-core/src/form/useRecordFromLocation.ts b/packages/ra-core/src/form/useRecordFromLocation.ts new file mode 100644 index 00000000000..3022858233d --- /dev/null +++ b/packages/ra-core/src/form/useRecordFromLocation.ts @@ -0,0 +1,71 @@ +import { parse } from 'query-string'; +import { Location, useLocation } from 'react-router-dom'; +import merge from 'lodash/merge'; +import { RaRecord } from '../types'; +import { useRecordContext } from '../controller'; + +/** + * A hook that returns the record to use as a form initial values. If a record is passed and the location search or state also contains a record, they will be merged. + * @param options The hook options + * @param options.record The record to use as initial values + * @param options.searchSource The key in the location search to use as a source for the record. Its content should be a stringified JSON object. + * @param options.stateSource The key in the location state to use as a source for the record + * @returns The record to use as initial values in a form + */ +export const useRecordFromLocation = ( + props: UseRecordFromLocationOptions = {} +) => { + const { searchSource, stateSource } = props; + const location = useLocation(); + const record = useRecordContext(props); + const recordFromLocation = getRecordFromLocation({ + location, + stateSource, + searchSource, + }); + + return merge({}, record, recordFromLocation); +}; + +export type UseRecordFromLocationOptions = { + record?: Partial; + searchSource?: string; + stateSource?: string; +}; + +/** + * Get the initial record from the location, whether it comes from the location + * state or is serialized in the url search part. + */ +export const getRecordFromLocation = ({ + location: { state, search }, + searchSource = 'source', + stateSource = 'record', +}: { + location: Location; + searchSource?: string; + stateSource?: string; +}) => { + if (state && state[stateSource]) { + return state[stateSource]; + } + if (search) { + try { + const searchParams = parse(search); + if (searchParams[searchSource]) { + if (Array.isArray(searchParams[searchSource])) { + console.error( + `Failed to parse location ${searchSource} parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified ${searchSource} parameter (e.g. '?${searchSource}={"title":"foo"}')` + ); + return; + } + return JSON.parse(searchParams[searchSource]); + } + } catch (e) { + console.error( + `Failed to parse location ${searchSource} parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified ${searchSource} parameter (e.g. '?${searchSource}={"title":"foo"}')` + ); + } + } + return null; +}; From 3b553cd07283cdc64abc3fccf17f96797eaff271 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:48:16 +0100 Subject: [PATCH 18/33] Use useRecordFromLocation in useAugmentedForm instead of useCreateController --- packages/ra-core/src/controller/create/useCreateController.ts | 4 ---- packages/ra-core/src/form/useAugmentedForm.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts index 5c1fff4e64e..3cd70914d90 100644 --- a/packages/ra-core/src/controller/create/useCreateController.ts +++ b/packages/ra-core/src/controller/create/useCreateController.ts @@ -21,7 +21,6 @@ import { useResourceDefinition, useGetResourceLabel, } from '../../core'; -import { useRecordFromLocation } from '../../form'; /** * Prepare data for the Create view @@ -53,7 +52,6 @@ export const useCreateController = < ): CreateControllerResult => { const { disableAuthentication, - record, redirect: redirectTo, transform, mutationOptions = {}, @@ -80,7 +78,6 @@ export const useCreateController = < const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); - const recordToUse = useRecordFromLocation({ record }); const { onSuccess, onError, meta, ...otherMutationOptions } = mutationOptions; const { @@ -198,7 +195,6 @@ export const useCreateController = < defaultTitle, save, resource, - record: recordToUse, redirect: finalRedirectTo, registerMutationMiddleware, unregisterMutationMiddleware, diff --git a/packages/ra-core/src/form/useAugmentedForm.ts b/packages/ra-core/src/form/useAugmentedForm.ts index d497e9a14b0..33cd6a3ddd7 100644 --- a/packages/ra-core/src/form/useAugmentedForm.ts +++ b/packages/ra-core/src/form/useAugmentedForm.ts @@ -8,7 +8,6 @@ import { import { RaRecord } from '../types'; import { SaveHandler, useSaveContext } from '../controller'; -import { useRecordContext } from '../controller'; import getFormInitialValues from './getFormInitialValues'; import { getSimpleValidationResolver, @@ -17,6 +16,7 @@ import { import { setSubmissionErrors } from './validation/setSubmissionErrors'; import { useNotifyIsFormInvalid } from './validation/useNotifyIsFormInvalid'; import { sanitizeEmptyValues as sanitizeValues } from './sanitizeEmptyValues'; +import { useRecordFromLocation } from './useRecordFromLocation'; /** * Wrapper around react-hook-form's useForm @@ -44,8 +44,8 @@ export const useAugmentedForm = ( disableInvalidFormNotification, ...rest } = props; - const record = useRecordContext(props); const saveContext = useSaveContext(); + const record = useRecordFromLocation(props); const defaultValuesIncludingRecord = useMemo( () => getFormInitialValues(defaultValues, record), From 56ec58181b7396cce6f7ae431387aef1ee208879 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:16:06 +0100 Subject: [PATCH 19/33] Fix build --- packages/ra-core/src/form/useRecordFromLocation.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ra-core/src/form/useRecordFromLocation.ts b/packages/ra-core/src/form/useRecordFromLocation.ts index 3022858233d..5bc3cce2ceb 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.ts +++ b/packages/ra-core/src/form/useRecordFromLocation.ts @@ -52,14 +52,15 @@ export const getRecordFromLocation = ({ if (search) { try { const searchParams = parse(search); - if (searchParams[searchSource]) { - if (Array.isArray(searchParams[searchSource])) { + const source = searchParams[searchSource]; + if (source) { + if (Array.isArray(source)) { console.error( `Failed to parse location ${searchSource} parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified ${searchSource} parameter (e.g. '?${searchSource}={"title":"foo"}')` ); return; } - return JSON.parse(searchParams[searchSource]); + return JSON.parse(source); } } catch (e) { console.error( From 8d8daea94cb8dc8b9d83b2557483b5c8ab2eccf2 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:36:34 +0100 Subject: [PATCH 20/33] Revert regression in useCreateController --- packages/ra-core/src/controller/create/useCreateController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts index 3cd70914d90..be3b2f7c633 100644 --- a/packages/ra-core/src/controller/create/useCreateController.ts +++ b/packages/ra-core/src/controller/create/useCreateController.ts @@ -52,6 +52,7 @@ export const useCreateController = < ): CreateControllerResult => { const { disableAuthentication, + record, redirect: redirectTo, transform, mutationOptions = {}, @@ -194,6 +195,7 @@ export const useCreateController = < saving, defaultTitle, save, + record, resource, redirect: finalRedirectTo, registerMutationMiddleware, From 3a60f733eba3033ad95b97b8a7c1ae12c4fe75a9 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:02:52 +0100 Subject: [PATCH 21/33] Revert breaking change on getRecordFromLocation --- .../src/form/useRecordFromLocation.spec.tsx | 39 +++++++++---------- .../ra-core/src/form/useRecordFromLocation.ts | 22 +++++------ 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx index 2dfa8992bd6..0ff36608f5c 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx +++ b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; +import { Location } from 'react-router-dom'; import { getRecordFromLocation, useRecordFromLocation, @@ -131,57 +132,53 @@ describe('getRecordFromLocation', () => { it('should return location state record when set', () => { expect( getRecordFromLocation({ - location: { - ...location, - state: { record: { foo: 'bar' } }, - }, + ...location, + state: { record: { foo: 'bar' } }, }) ).toEqual({ foo: 'bar' }); }); it('should return location state record when set with a custom key', () => { expect( - getRecordFromLocation({ - location: { + getRecordFromLocation( + { ...location, state: { myRecord: { foo: 'bar' } }, }, - stateSource: 'myRecord', - }) + { stateSource: 'myRecord' } + ) ).toEqual({ foo: 'bar' }); }); it('should return location search when set', () => { expect( getRecordFromLocation({ - location: { - ...location, - search: '?source={"foo":"baz","array":["1","2"]}', - }, + ...location, + search: '?source={"foo":"baz","array":["1","2"]}', }) ).toEqual({ foo: 'baz', array: ['1', '2'] }); }); it('should return location search when set with a custom key', () => { expect( - getRecordFromLocation({ - location: { + getRecordFromLocation( + { ...location, search: '?mySource={"foo":"baz","array":["1","2"]}', }, - searchSource: 'mySource', - }) + { + searchSource: 'mySource', + } + ) ).toEqual({ foo: 'baz', array: ['1', '2'] }); }); it('should return location state record when both state and search are set', () => { expect( getRecordFromLocation({ - location: { - ...location, - state: { record: { foo: 'bar' } }, - search: '?foo=baz', - }, + ...location, + state: { record: { foo: 'bar' } }, + search: '?foo=baz', }) ).toEqual({ foo: 'bar' }); }); diff --git a/packages/ra-core/src/form/useRecordFromLocation.ts b/packages/ra-core/src/form/useRecordFromLocation.ts index 5bc3cce2ceb..82188ef9b7f 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.ts +++ b/packages/ra-core/src/form/useRecordFromLocation.ts @@ -18,8 +18,7 @@ export const useRecordFromLocation = ( const { searchSource, stateSource } = props; const location = useLocation(); const record = useRecordContext(props); - const recordFromLocation = getRecordFromLocation({ - location, + const recordFromLocation = getRecordFromLocation(location, { stateSource, searchSource, }); @@ -37,15 +36,16 @@ export type UseRecordFromLocationOptions = { * Get the initial record from the location, whether it comes from the location * state or is serialized in the url search part. */ -export const getRecordFromLocation = ({ - location: { state, search }, - searchSource = 'source', - stateSource = 'record', -}: { - location: Location; - searchSource?: string; - stateSource?: string; -}) => { +export const getRecordFromLocation = ( + { state, search }: Location, + { + searchSource = 'source', + stateSource = 'record', + }: { + searchSource?: string; + stateSource?: string; + } = {} +) => { if (state && state[stateSource]) { return state[stateSource]; } From db4823ba46dcd7d419e2bc075e6fbec78eb776f5 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:23:52 +0100 Subject: [PATCH 22/33] Add story for edition --- .../edit/useEditController.stories.tsx | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/controller/edit/useEditController.stories.tsx b/packages/ra-core/src/controller/edit/useEditController.stories.tsx index 74f3b86eb1a..b5e4e05c873 100644 --- a/packages/ra-core/src/controller/edit/useEditController.stories.tsx +++ b/packages/ra-core/src/controller/edit/useEditController.stories.tsx @@ -2,9 +2,13 @@ import * as React from 'react'; import { Route, Routes, useLocation } from 'react-router'; import { CoreAdminContext, + EditBase, EditController, + Form, + InputProps, testDataProvider, TestMemoryRouter, + useInput, } from '../..'; export default { @@ -75,11 +79,93 @@ export const EncodedIdWithPercentage = ({ ); }; -const LocationInspector = () => { +export const OverrideRecordWithLocation = ({ + url = '/posts/1', + dataProvider = testDataProvider({ + // @ts-expect-error + getOne: () => + Promise.resolve({ + data: { id: 1, title: 'hello', value: 'a value' }, + }), + }), +}) => { + return ( + + + + +
    + +
    +
    + + +
    +
    +
    + + } + /> +
    +
    +
    + ); +}; + +OverrideRecordWithLocation.argTypes = { + url: { + options: [ + 'unmodified', + 'modified with location state', + 'modified with location search', + ], + mapping: { + unmodified: '/posts/1', + 'modified with location state': { + pathname: '/posts/1', + state: { record: { value: 'from-state' } }, + }, + 'modified with location search': `/posts/1?source=${encodeURIComponent(JSON.stringify({ value: 'from-search' }))}`, + }, + control: { type: 'select' }, + }, +}; + +const LocationInspector = ({ deep }: { deep?: boolean }) => { const location = useLocation(); return (

    - Location: {location.pathname} + Location:{' '} + {deep ? JSON.stringify(location) : location.pathname}

    ); }; + +const TextInput = (props: InputProps) => { + const input = useInput(props); + + return ( +
    + + +
    + ); +}; From 3ec1068e675e4391b5a5b00f7a4e2ec460091401 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:51:02 +0100 Subject: [PATCH 23/33] Use react-hook-form reset to override the record --- packages/ra-core/src/form/Form.spec.tsx | 32 +++++ packages/ra-core/src/form/Form.stories.tsx | 114 +++++++++++++++++- packages/ra-core/src/form/useAugmentedForm.ts | 26 +++- .../src/form/useRecordFromLocation.spec.tsx | 74 ++---------- .../ra-core/src/form/useRecordFromLocation.ts | 43 ++++--- 5 files changed, 205 insertions(+), 84 deletions(-) diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index fef541ead55..41ca74d265a 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -20,8 +20,10 @@ import { NullValue, InNonDataRouter, ServerSideValidation, + MultiRoutesForm, } from './Form.stories'; import { mergeTranslations } from '../i18n'; +import { To } from 'react-router'; describe('Form', () => { const Input = props => { @@ -791,4 +793,34 @@ describe('Form', () => { 'There are validation errors. Please fix them.' ); }); + + it.each([ + { + from: 'state', + url: { + pathname: '/form/general', + state: { record: { body: 'from-state' } }, + }, + expectedValue: 'from-state', + }, + { + from: 'search query', + url: `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}` as To, + expectedValue: 'from-search', + }, + ])( + 'should support overriding the record values from the location $from', + async ({ url, expectedValue }) => { + render(); + await screen.findByDisplayValue('lorem'); + expect( + (screen.getByText('Submit') as HTMLInputElement).disabled + ).toEqual(false); + fireEvent.click(screen.getByText('Settings')); + await screen.findByDisplayValue(expectedValue); + expect( + (screen.getByText('Submit') as HTMLInputElement).disabled + ).toEqual(false); + } + ); }); diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index 32402820bbb..69c4a165d30 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -8,7 +8,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; -import { Route, Routes, useNavigate, Link, HashRouter } from 'react-router-dom'; +import { + Route, + Routes, + useNavigate, + Link, + HashRouter, + useLocation, +} from 'react-router-dom'; import { CoreAdminContext } from '../core'; import { Form } from './Form'; @@ -16,7 +23,11 @@ import { useInput } from './useInput'; import { required, ValidationError } from './validation'; import { mergeTranslations } from '../i18n'; import { I18nProvider } from '../types'; -import { SaveContextProvider, useNotificationContext } from '..'; +import { + SaveContextProvider, + TestMemoryRouter, + useNotificationContext, +} from '..'; export default { title: 'ra-core/form/Form', @@ -403,3 +414,102 @@ export const ServerSideValidation = () => { ); }; + +export const MultiRoutesForm = ({ url }: { url?: any }) => ( + + + + } /> + + + +); + +MultiRoutesForm.args = { + url: 'unmodified', +}; + +MultiRoutesForm.argTypes = { + url: { + options: [ + 'unmodified', + 'modified with location state', + 'modified with location search', + ], + mapping: { + unmodified: '/form/general', + 'modified with location state': { + pathname: '/form/general', + state: { record: { body: 'from-state' } }, + }, + 'modified with location search': `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}`, + }, + control: { type: 'select' }, + }, +}; + +const record = { title: 'lorem', body: 'unmodified' }; +const FormWithSubRoutes = () => { + return ( + <> +
    + + + + + ); +}; + +const TabbedForm = () => { + const location = useLocation(); + + return ( + <> +
    + + General + + + Settings + +
    + + + + + + + + ); +}; +const Tab = ({ + children, + name, +}: { + children: React.ReactNode; + name: string; +}) => { + const location = useLocation(); + + return ( +
    + {children} +
    + ); +}; diff --git a/packages/ra-core/src/form/useAugmentedForm.ts b/packages/ra-core/src/form/useAugmentedForm.ts index 33cd6a3ddd7..393cd50d0e5 100644 --- a/packages/ra-core/src/form/useAugmentedForm.ts +++ b/packages/ra-core/src/form/useAugmentedForm.ts @@ -1,13 +1,19 @@ -import { BaseSyntheticEvent, useCallback, useMemo, useRef } from 'react'; +import { + BaseSyntheticEvent, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; import { FieldValues, SubmitHandler, useForm, UseFormProps, } from 'react-hook-form'; - +import merge from 'lodash/merge'; import { RaRecord } from '../types'; -import { SaveHandler, useSaveContext } from '../controller'; +import { SaveHandler, useRecordContext, useSaveContext } from '../controller'; import getFormInitialValues from './getFormInitialValues'; import { getSimpleValidationResolver, @@ -45,7 +51,7 @@ export const useAugmentedForm = ( ...rest } = props; const saveContext = useSaveContext(); - const record = useRecordFromLocation(props); + const record = useRecordContext(props); const defaultValuesIncludingRecord = useMemo( () => getFormInitialValues(defaultValues, record), @@ -81,6 +87,18 @@ export const useAugmentedForm = ( // notify on invalid form useNotifyIsFormInvalid(form.control, !disableInvalidFormNotification); + const recordFromLocation = useRecordFromLocation(); + const recordFromLocationApplied = useRef(false); + const { reset } = form; + useEffect(() => { + if (recordFromLocation && !recordFromLocationApplied.current) { + reset(merge({}, record, recordFromLocation), { + keepDefaultValues: true, + }); + recordFromLocationApplied.current = true; + } + }, [record, recordFromLocation, reset]); + // submit callbacks const handleSubmit = useCallback( async (values, event) => { diff --git a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx index 0ff36608f5c..e20d0486593 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx +++ b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx @@ -5,11 +5,7 @@ import { getRecordFromLocation, useRecordFromLocation, } from './useRecordFromLocation'; -import { - RecordContextProvider, - TestMemoryRouter, - UseRecordFromLocationOptions, -} from '..'; +import { TestMemoryRouter, UseRecordFromLocationOptions } from '..'; describe('useRecordFromLocation', () => { const UseGetRecordFromLocation = (props: UseRecordFromLocationOptions) => { @@ -31,54 +27,16 @@ describe('useRecordFromLocation', () => { await screen.findByText(JSON.stringify(record)); }); - it('return the record from the RecordContext as is if there is no location search nor state that contains a record', async () => { + it('return null if there is no location search nor state that contains a record', async () => { render( - - - - - ); - - await screen.findByText(JSON.stringify({ initial: 'initial value' })); - }); - it('return merge the record from the RecordContext with the one from the location search', async () => { - const record = { test: 'value' }; - render( - - - - - - ); - - await screen.findByText( - JSON.stringify({ initial: 'initial value', test: 'value' }) - ); - }); - it('return merge the record from the RecordContext with the one from the location state', async () => { - const record = { test: 'value' }; - render( - - - - + ); - await screen.findByText( - JSON.stringify({ initial: 'initial value', test: 'value' }) - ); + await screen.findByText('null'); }); - it('return merge the record passed as option with the one from the location search', async () => { + it('return the record from the location search', async () => { const record = { test: 'value' }; render( { `/posts/create?source=${JSON.stringify(record)}`, ]} > - - - + ); - await screen.findByText( - JSON.stringify({ anotherRecord: 'another value', test: 'value' }) - ); + await screen.findByText(JSON.stringify({ test: 'value' })); }); - it('return merge the record passed as option with the one from the location state', async () => { + it('return the record from the location state', async () => { const record = { test: 'value' }; render( { { pathname: `/posts/create`, state: { record } }, ]} > - - - + ); - await screen.findByText( - JSON.stringify({ anotherRecord: 'another value', test: 'value' }) - ); + await screen.findByText(JSON.stringify({ test: 'value' })); }); }); diff --git a/packages/ra-core/src/form/useRecordFromLocation.ts b/packages/ra-core/src/form/useRecordFromLocation.ts index 82188ef9b7f..6b4d19ef459 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.ts +++ b/packages/ra-core/src/form/useRecordFromLocation.ts @@ -1,33 +1,48 @@ +import { useEffect, useRef, useState } from 'react'; import { parse } from 'query-string'; import { Location, useLocation } from 'react-router-dom'; -import merge from 'lodash/merge'; +import isEqual from 'lodash/isEqual'; import { RaRecord } from '../types'; -import { useRecordContext } from '../controller'; /** - * A hook that returns the record to use as a form initial values. If a record is passed and the location search or state also contains a record, they will be merged. + * A hook that returns the record to use to override the values in a form * @param options The hook options - * @param options.record The record to use as initial values * @param options.searchSource The key in the location search to use as a source for the record. Its content should be a stringified JSON object. * @param options.stateSource The key in the location state to use as a source for the record - * @returns The record to use as initial values in a form + * @returns The record to use to override the values in a form */ export const useRecordFromLocation = ( props: UseRecordFromLocationOptions = {} ) => { const { searchSource, stateSource } = props; const location = useLocation(); - const record = useRecordContext(props); - const recordFromLocation = getRecordFromLocation(location, { - stateSource, - searchSource, - }); + const [recordFromLocation, setRecordFromLocation] = useState(() => + getRecordFromLocation(location, { + stateSource, + searchSource, + }) + ); - return merge({}, record, recordFromLocation); + // To avoid having the form resets when the location changes but the final record is the same + // This is needed for forms such as TabbedForm or WizardForm that may change the location for their sections + const finalRecordRef = useRef(recordFromLocation); + + useEffect(() => { + const newRecordFromLocation = getRecordFromLocation(location, { + stateSource, + searchSource, + }); + + if (!isEqual(newRecordFromLocation, finalRecordRef.current)) { + finalRecordRef.current = newRecordFromLocation; + setRecordFromLocation(newRecordFromLocation); + } + }, [location, stateSource, searchSource]); + + return recordFromLocation; }; export type UseRecordFromLocationOptions = { - record?: Partial; searchSource?: string; stateSource?: string; }; @@ -45,7 +60,7 @@ export const getRecordFromLocation = ( searchSource?: string; stateSource?: string; } = {} -) => { +): Partial | null => { if (state && state[stateSource]) { return state[stateSource]; } @@ -58,7 +73,7 @@ export const getRecordFromLocation = ( console.error( `Failed to parse location ${searchSource} parameter '${search}'. To pre-fill some fields in the Create form, pass a stringified ${searchSource} parameter (e.g. '?${searchSource}={"title":"foo"}')` ); - return; + return null; } return JSON.parse(source); } From 003f7f929b7fb44ab869f88da189566635924afe Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:41:51 +0100 Subject: [PATCH 24/33] Add documentation --- docs/Edit.md | 55 ++++++++++++++++++++++++++++++ docs/useGetRecordRepresentation.md | 2 +- docs/useRecordFromLocation.md | 55 ++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 docs/useRecordFromLocation.md diff --git a/docs/Edit.md b/docs/Edit.md index 354d7616553..cd08ce075de 100644 --- a/docs/Edit.md +++ b/docs/Edit.md @@ -787,6 +787,61 @@ You can do the same for error notifications, by passing a custom `onError` call **Tip**: The notification message will be translated. +## Prefilling the Form + +You sometimes need to pre-populate the form changes to a record based on a *related* record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others as well. + +By default, the `` view starts with the current `record`. However, if the `location` object (injected by [react-router-dom](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` to prefill the form. + +That means that if you want to create a link to edition view, modifying immediately *some* values, all you have to do is to set the `state` prop of the ``: + +{% raw %} +```jsx +import * as React from 'react'; +import { EditButton, Datagrid, List, useRecordContext } from 'react-admin'; + +const ApproveButton = () => { + const record = useRecordContext(); + return ( + + ); +}; + +export default PostList = () => ( + + + ... + + + +) +``` +{% endraw %} + +**Tip**: The `` component also watches the "source" parameter of `location.search` (the query string in the URL) in addition to `location.state` (a cross-page message hidden in the router memory). So the `ApproveButton` could also be written as: + +{% raw %} +```jsx +import * as React from 'react'; +import { CreateButton, useRecordContext } from 'react-admin'; + +const ApproveButton = () => { + const record = useRecordContext(); + return ( + + ); +}; +``` +{% endraw %} + +Should you use the location `state` or the location `search`? The latter modifies the URL, so it's only necessary if you want to build cross-application links (e.g. from one admin to the other). In general, using the location `state` is a safe bet. + ## Editing A Record In A Modal `` is designed to be a page component, passed to the `edit` prop of the `` component. But you may want to let users edit a record from another page. diff --git a/docs/useGetRecordRepresentation.md b/docs/useGetRecordRepresentation.md index 35ce7a05394..90ca4d5248e 100644 --- a/docs/useGetRecordRepresentation.md +++ b/docs/useGetRecordRepresentation.md @@ -1,6 +1,6 @@ --- layout: default -title: "The useGetRecordRepresentation Component" +title: "The useGetRecordRepresentation Hook" --- # `useGetRecordRepresentation` diff --git a/docs/useRecordFromLocation.md b/docs/useRecordFromLocation.md new file mode 100644 index 00000000000..f13922c4a0f --- /dev/null +++ b/docs/useRecordFromLocation.md @@ -0,0 +1,55 @@ +--- +layout: default +title: "The useRecordFromLocation Hook" +--- + +# `useRecordFromLocation` + +Return a record that was passed through either [the location query or the location state](https://reactrouter.com/6.28.0/start/concepts#locations). + +You may use it to know whether the form values of the current create or edit view have been overridden from the location as supported by the [`Create`](./Create.md#prefilling-the-form) and [`Edit`](./Edit.md#prefilling-the-form) components. + +## Usage + +```tsx +// in src/posts/PostEdit.tsx +import * as React from 'react'; +import { Alert } from '@mui/material'; +import { Edit, SimpleForm, TextInput, useRecordFromLocation } from 'react-admin'; + +export const PostEdit = () => { + const recordFromLocation = useRecordFromLocation(); + return ( + + {recordFromLocation + ? ( + + The record has been modified. + + ) + : null + } + + + + + ); +} +``` + +## Options + +Here are all the options you can set on the `useRecordFromLocation` hook: + +| Prop | Required | Type | Default | Description | +| -------------- | -------- | ---------- | ---------- | -------------------------------------------------------------------------------- | +| `searchSource` | | `string` | `'source'` | The name of the location search parameter that may contains a stringified record | +| `stateSource` | | `string` | `'record'` | The name of the location state parameter that may contains a stringified record | + +## `searchSource` + +The name of the [location search](https://reactrouter.com/6.28.0/start/concepts#locations) parameter that may contains a stringified record. Defaults to `source`. + +## `stateSource` + +The name of the [location state](https://reactrouter.com/6.28.0/start/concepts#locations) parameter that may contains a stringified record. Defaults to `record`. From 10b463318a188446837a09880887ef0d46019378 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:09:40 +0100 Subject: [PATCH 25/33] Add more tests --- packages/ra-core/src/form/Form.spec.tsx | 40 ++++++++++++++++++++-- packages/ra-core/src/form/Form.stories.tsx | 40 ++++++++++++++++------ 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index 41ca74d265a..fb6fd9e8929 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -809,18 +809,54 @@ describe('Form', () => { expectedValue: 'from-search', }, ])( - 'should support overriding the record values from the location $from', + 'should support prefilling the from values from the location $from', async ({ url, expectedValue }) => { render(); - await screen.findByDisplayValue('lorem'); + expect( + (await screen.findByLabelText('title')).value + ).toEqual(''); expect( (screen.getByText('Submit') as HTMLInputElement).disabled ).toEqual(false); fireEvent.click(screen.getByText('Settings')); await screen.findByDisplayValue(expectedValue); + expect( + screen.getByText('Submit').disabled + ).toEqual(false); + } + ); + it.each([ + { + from: 'state', + url: { + pathname: '/form/general', + state: { record: { body: 'from-state' } }, + }, + expectedValue: 'from-state', + }, + { + from: 'search query', + url: `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}` as To, + expectedValue: 'from-search', + }, + ])( + 'should support overriding the record values from the location $from', + async ({ url, expectedValue }) => { + render( + + ); + await screen.findByDisplayValue('lorem'); expect( (screen.getByText('Submit') as HTMLInputElement).disabled ).toEqual(false); + fireEvent.click(screen.getByText('Settings')); + await screen.findByDisplayValue(expectedValue); + expect( + screen.getByText('Submit').disabled + ).toEqual(false); } ); }); diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index 69c4a165d30..62386fe9578 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -22,8 +22,9 @@ import { Form } from './Form'; import { useInput } from './useInput'; import { required, ValidationError } from './validation'; import { mergeTranslations } from '../i18n'; -import { I18nProvider } from '../types'; +import { I18nProvider, RaRecord } from '../types'; import { + RecordContextProvider, SaveContextProvider, TestMemoryRouter, useNotificationContext, @@ -415,11 +416,24 @@ export const ServerSideValidation = () => { ); }; -export const MultiRoutesForm = ({ url }: { url?: any }) => ( +export const MultiRoutesForm = ({ + url, + initialRecord, +}: { + url?: any; + initialRecord?: Partial; +}) => ( - } /> + + + + } + /> @@ -427,6 +441,7 @@ export const MultiRoutesForm = ({ url }: { url?: any }) => ( MultiRoutesForm.args = { url: 'unmodified', + initialRecord: 'none', }; MultiRoutesForm.argTypes = { @@ -446,17 +461,22 @@ MultiRoutesForm.argTypes = { }, control: { type: 'select' }, }, + initialRecord: { + options: ['none', 'provided'], + mapping: { + none: undefined, + provided: { title: 'lorem', body: 'unmodified' }, + }, + control: { type: 'select' }, + }, }; -const record = { title: 'lorem', body: 'unmodified' }; const FormWithSubRoutes = () => { return ( - <> -
    - - - - +
    + + + ); }; From ce94e008c687ee2f8e22a7da7b6ef5d62fb539da Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:12:37 +0100 Subject: [PATCH 26/33] Improve documentation [no ci] --- docs/Edit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Edit.md b/docs/Edit.md index cd08ce075de..417bce2801e 100644 --- a/docs/Edit.md +++ b/docs/Edit.md @@ -789,11 +789,11 @@ You can do the same for error notifications, by passing a custom `onError` call ## Prefilling the Form -You sometimes need to pre-populate the form changes to a record based on a *related* record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others as well. +You sometimes need to pre-populate the form changes to a record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others fields as well. By default, the `` view starts with the current `record`. However, if the `location` object (injected by [react-router-dom](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` to prefill the form. -That means that if you want to create a link to edition view, modifying immediately *some* values, all you have to do is to set the `state` prop of the ``: +That means that if you want to create a link to an edition view, modifying immediately *some* values, all you have to do is to set the `state` prop of the ``: {% raw %} ```jsx From 03a1fb1e7cff671c86cca80031feaf9cf1aac56d Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:16:42 +0100 Subject: [PATCH 27/33] Improve imports --- packages/ra-core/src/core/SourceContext.stories.tsx | 2 +- packages/ra-core/src/form/Form.spec.tsx | 2 +- packages/ra-core/src/form/Form.stories.tsx | 9 +++------ packages/ra-core/src/form/FormDataConsumer.spec.tsx | 4 +++- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/ra-core/src/core/SourceContext.stories.tsx b/packages/ra-core/src/core/SourceContext.stories.tsx index 839398bc635..90068f2ee56 100644 --- a/packages/ra-core/src/core/SourceContext.stories.tsx +++ b/packages/ra-core/src/core/SourceContext.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { Form, useInput } from '../form'; -import { TestMemoryRouter } from '..'; +import { TestMemoryRouter } from '../routing'; export default { title: 'ra-core/core/SourceContext', diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index fb6fd9e8929..bec86cb8757 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -6,6 +6,7 @@ import * as yup from 'yup'; import assert from 'assert'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; +import type { To } from 'react-router'; import { CoreAdminContext } from '../core'; import { Form } from './Form'; @@ -23,7 +24,6 @@ import { MultiRoutesForm, } from './Form.stories'; import { mergeTranslations } from '../i18n'; -import { To } from 'react-router'; describe('Form', () => { const Input = props => { diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index 62386fe9578..63c2d487772 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -18,17 +18,14 @@ import { } from 'react-router-dom'; import { CoreAdminContext } from '../core'; +import { RecordContextProvider, SaveContextProvider } from '../controller'; import { Form } from './Form'; import { useInput } from './useInput'; import { required, ValidationError } from './validation'; import { mergeTranslations } from '../i18n'; import { I18nProvider, RaRecord } from '../types'; -import { - RecordContextProvider, - SaveContextProvider, - TestMemoryRouter, - useNotificationContext, -} from '..'; +import { TestMemoryRouter } from '../routing'; +import { useNotificationContext } from '../notification'; export default { title: 'ra-core/form/Form', diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index d7ffd7497ca..6498b370990 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -12,7 +12,9 @@ import { ArrayInput, } from 'ra-ui-materialui'; import expect from 'expect'; -import { Form, ResourceContextProvider, TestMemoryRouter } from '..'; +import { ResourceContextProvider } from '../core'; +import { Form } from '../form'; +import { TestMemoryRouter } from '../routing'; describe('FormDataConsumerView', () => { it('does not call its children function with scopedFormData if it did not receive a source containing an index', () => { From 23e0c63fb0a026f994e43cd11586265d6b173b2a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Tue, 17 Dec 2024 11:26:54 +0100 Subject: [PATCH 28/33] Fix code scanning alert no. 43: Prototype-polluting merge call --- packages/ra-core/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 817f830df58..8c460b9eb63 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -64,7 +64,7 @@ "eventemitter3": "^5.0.1", "inflection": "^3.0.0", "jsonexport": "^3.2.0", - "lodash": "~4.17.5", + "lodash": "^4.17.21", "query-string": "^7.1.3", "react-error-boundary": "^4.0.13", "react-is": "^18.2.0" diff --git a/yarn.lock b/yarn.lock index a87a2227af5..7687f3f8815 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15847,7 +15847,7 @@ __metadata: inflection: "npm:^3.0.0" jscodeshift: "npm:^0.15.2" jsonexport: "npm:^3.2.0" - lodash: "npm:~4.17.5" + lodash: "npm:^4.17.21" query-string: "npm:^7.1.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" From 437bfa297341a7a0e97ae853f7413dc1f634a0c4 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:18:49 +0100 Subject: [PATCH 29/33] Improve documentation --- docs/Create.md | 2 +- docs/Edit.md | 8 +++----- docs/Reference.md | 1 + docs/navigation.html | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Create.md b/docs/Create.md index 9084bfa9a74..1b94960e32b 100644 --- a/docs/Create.md +++ b/docs/Create.md @@ -441,7 +441,7 @@ You can do the same for error notifications, by passing a custom `onError` call You sometimes need to pre-populate a record based on a *related* record. For instance, to create a comment related to an existing post. -By default, the `` view starts with an empty `record`. However, if the `location` object (injected by [react-router-dom](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` instead of the empty object. That's how the `` works under the hood. +By default, the `` view starts with an empty `record`. However, if the `location` object (injected by [react-router-dom](https://reactrouter.com/6.28.0/start/concepts#locations)) contains a `record` in its `state`, the `` view uses that `record` instead of the empty object. That's how the `` works under the hood. That means that if you want to create a link to a creation form, presetting *some* values, all you have to do is to set the `state` prop of the ``: diff --git a/docs/Edit.md b/docs/Edit.md index 417bce2801e..d12288e55f4 100644 --- a/docs/Edit.md +++ b/docs/Edit.md @@ -791,17 +791,16 @@ You can do the same for error notifications, by passing a custom `onError` call You sometimes need to pre-populate the form changes to a record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others fields as well. -By default, the `` view starts with the current `record`. However, if the `location` object (injected by [react-router-dom](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` to prefill the form. +By default, the `` view starts with the current `record`. However, if the `location` object (injected by [react-router-dom](https://reactrouter.com/6.28.0/start/concepts#locations)) contains a `record` in its `state`, the `` view uses that `record` to prefill the form. That means that if you want to create a link to an edition view, modifying immediately *some* values, all you have to do is to set the `state` prop of the ``: {% raw %} ```jsx import * as React from 'react'; -import { EditButton, Datagrid, List, useRecordContext } from 'react-admin'; +import { EditButton, Datagrid, List } from 'react-admin'; const ApproveButton = () => { - const record = useRecordContext(); return ( ( {% raw %} ```jsx import * as React from 'react'; -import { CreateButton, useRecordContext } from 'react-admin'; +import { EditButton } from 'react-admin'; const ApproveButton = () => { - const record = useRecordContext(); return ( useEditContext
  • useEditController
  • useSaveContext
  • +
  • useRecordFromLocation
  • useRegisterMutationMiddleware
  • useUnique
From be8eab81b27de142cbfd3950c2bb63e6c2669119 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:18:55 +0100 Subject: [PATCH 30/33] Remove unnecessary story --- .../edit/useEditController.stories.tsx | 90 +------------------ 1 file changed, 2 insertions(+), 88 deletions(-) diff --git a/packages/ra-core/src/controller/edit/useEditController.stories.tsx b/packages/ra-core/src/controller/edit/useEditController.stories.tsx index b5e4e05c873..74f3b86eb1a 100644 --- a/packages/ra-core/src/controller/edit/useEditController.stories.tsx +++ b/packages/ra-core/src/controller/edit/useEditController.stories.tsx @@ -2,13 +2,9 @@ import * as React from 'react'; import { Route, Routes, useLocation } from 'react-router'; import { CoreAdminContext, - EditBase, EditController, - Form, - InputProps, testDataProvider, TestMemoryRouter, - useInput, } from '../..'; export default { @@ -79,93 +75,11 @@ export const EncodedIdWithPercentage = ({ ); }; -export const OverrideRecordWithLocation = ({ - url = '/posts/1', - dataProvider = testDataProvider({ - // @ts-expect-error - getOne: () => - Promise.resolve({ - data: { id: 1, title: 'hello', value: 'a value' }, - }), - }), -}) => { - return ( - - - - -
- -
-
- - -
-
-
- - } - /> -
-
-
- ); -}; - -OverrideRecordWithLocation.argTypes = { - url: { - options: [ - 'unmodified', - 'modified with location state', - 'modified with location search', - ], - mapping: { - unmodified: '/posts/1', - 'modified with location state': { - pathname: '/posts/1', - state: { record: { value: 'from-state' } }, - }, - 'modified with location search': `/posts/1?source=${encodeURIComponent(JSON.stringify({ value: 'from-search' }))}`, - }, - control: { type: 'select' }, - }, -}; - -const LocationInspector = ({ deep }: { deep?: boolean }) => { +const LocationInspector = () => { const location = useLocation(); return (

- Location:{' '} - {deep ? JSON.stringify(location) : location.pathname} + Location: {location.pathname}

); }; - -const TextInput = (props: InputProps) => { - const input = useInput(props); - - return ( -
- - -
- ); -}; From 994e5b75a89fbaa48dad3ba1126a04ff8dc3c057 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:19:12 +0100 Subject: [PATCH 31/33] Remove duplicate test --- .../src/form/useRecordFromLocation.spec.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx index e20d0486593..f8c9654de9c 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.spec.tsx +++ b/packages/ra-core/src/form/useRecordFromLocation.spec.tsx @@ -13,20 +13,6 @@ describe('useRecordFromLocation', () => { return
{JSON.stringify(recordFromLocation)}
; }; - it('return the record from the location search', async () => { - const record = { test: 'value' }; - render( - - - - ); - - await screen.findByText(JSON.stringify(record)); - }); it('return null if there is no location search nor state that contains a record', async () => { render( From 46a15e6c5e4eebc3c0b5f0809f84214f1ab5be10 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:19:21 +0100 Subject: [PATCH 32/33] Renaming --- packages/ra-core/src/form/useRecordFromLocation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ra-core/src/form/useRecordFromLocation.ts b/packages/ra-core/src/form/useRecordFromLocation.ts index 6b4d19ef459..d071596038d 100644 --- a/packages/ra-core/src/form/useRecordFromLocation.ts +++ b/packages/ra-core/src/form/useRecordFromLocation.ts @@ -25,7 +25,7 @@ export const useRecordFromLocation = ( // To avoid having the form resets when the location changes but the final record is the same // This is needed for forms such as TabbedForm or WizardForm that may change the location for their sections - const finalRecordRef = useRef(recordFromLocation); + const previousRecordRef = useRef(recordFromLocation); useEffect(() => { const newRecordFromLocation = getRecordFromLocation(location, { @@ -33,8 +33,8 @@ export const useRecordFromLocation = ( searchSource, }); - if (!isEqual(newRecordFromLocation, finalRecordRef.current)) { - finalRecordRef.current = newRecordFromLocation; + if (!isEqual(newRecordFromLocation, previousRecordRef.current)) { + previousRecordRef.current = newRecordFromLocation; setRecordFromLocation(newRecordFromLocation); } }, [location, stateSource, searchSource]); From b2026b8f9cf4af541467085ac45c14ef49afd9c8 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:19:32 +0100 Subject: [PATCH 33/33] Ensure we include default values --- packages/ra-core/src/form/Form.spec.tsx | 26 +++++++++++++++++- packages/ra-core/src/form/Form.stories.tsx | 27 ++++++++++++++----- packages/ra-core/src/form/useAugmentedForm.ts | 4 +-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index bec86cb8757..a3fd7aa02b5 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -833,22 +833,46 @@ describe('Form', () => { state: { record: { body: 'from-state' } }, }, expectedValue: 'from-state', + expectedDefaultValue: '', + }, + { + from: 'state with default values', + url: { + pathname: '/form/general', + state: { record: { body: 'from-state' } }, + }, + expectedValue: 'from-state', + defaultValues: { category: 'default category' }, + expectedDefaultValue: 'default category', }, { from: 'search query', url: `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}` as To, expectedValue: 'from-search', + expectedDefaultValue: '', + }, + { + from: 'search query with default values', + url: `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}` as To, + expectedValue: 'from-search', + defaultValues: { category: 'default category' }, + expectedDefaultValue: 'default category', }, ])( 'should support overriding the record values from the location $from', - async ({ url, expectedValue }) => { + async ({ url, defaultValues, expectedValue, expectedDefaultValue }) => { render( ); await screen.findByDisplayValue('lorem'); + expect( + (await screen.findByLabelText('category')) + .value + ).toEqual(expectedDefaultValue); expect( (screen.getByText('Submit') as HTMLInputElement).disabled ).toEqual(false); diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index 63c2d487772..0570b36fbd3 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -19,7 +19,7 @@ import { import { CoreAdminContext } from '../core'; import { RecordContextProvider, SaveContextProvider } from '../controller'; -import { Form } from './Form'; +import { Form, FormProps } from './Form'; import { useInput } from './useInput'; import { required, ValidationError } from './validation'; import { mergeTranslations } from '../i18n'; @@ -58,10 +58,12 @@ const Input = props => { }; const SubmitButton = () => { - const state = useFormState(); + const { dirtyFields } = useFormState(); + // useFormState().isDirty might differ from useFormState().dirtyFields (https://github.com/react-hook-form/react-hook-form/issues/4740) + const isDirty = Object.keys(dirtyFields).length > 0; return ( - ); @@ -416,9 +418,11 @@ export const ServerSideValidation = () => { export const MultiRoutesForm = ({ url, initialRecord, + defaultValues, }: { url?: any; initialRecord?: Partial; + defaultValues?: Partial; }) => ( @@ -427,7 +431,7 @@ export const MultiRoutesForm = ({ path="/form/*" element={ - + } /> @@ -458,6 +462,16 @@ MultiRoutesForm.argTypes = { }, control: { type: 'select' }, }, + defaultValues: { + options: ['none', 'provided'], + mapping: { + none: undefined, + provided: { + category: 'default category', + }, + }, + control: { type: 'select' }, + }, initialRecord: { options: ['none', 'provided'], mapping: { @@ -468,9 +482,9 @@ MultiRoutesForm.argTypes = { }, }; -const FormWithSubRoutes = () => { +const FormWithSubRoutes = (props: Partial) => { return ( -
+ @@ -502,6 +516,7 @@ const TabbedForm = () => { + diff --git a/packages/ra-core/src/form/useAugmentedForm.ts b/packages/ra-core/src/form/useAugmentedForm.ts index 393cd50d0e5..6f47a36fb43 100644 --- a/packages/ra-core/src/form/useAugmentedForm.ts +++ b/packages/ra-core/src/form/useAugmentedForm.ts @@ -92,12 +92,12 @@ export const useAugmentedForm = ( const { reset } = form; useEffect(() => { if (recordFromLocation && !recordFromLocationApplied.current) { - reset(merge({}, record, recordFromLocation), { + reset(merge({}, defaultValuesIncludingRecord, recordFromLocation), { keepDefaultValues: true, }); recordFromLocationApplied.current = true; } - }, [record, recordFromLocation, reset]); + }, [defaultValuesIncludingRecord, recordFromLocation, reset]); // submit callbacks const handleSubmit = useCallback(