From 39602dfcd7f5d4f84eaa01958551a803e8a5e2b7 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 15 Nov 2023 08:01:39 +0100 Subject: [PATCH 01/72] [Doc] Add headless section in pages components --- docs/Create.md | 89 +++++++++++ docs/Edit.md | 89 +++++++++++ docs/EditBase.md | 29 ++-- docs/List.md | 149 ++++++++++++++++++ docs/Show.md | 94 ++++++++--- docs/SimpleForm.md | 32 ++++ docs/useCreateController.md | 51 +++--- docs/useEditController.md | 9 +- .../src/form/SimpleForm.stories.tsx | 24 ++- 9 files changed, 505 insertions(+), 61 deletions(-) diff --git a/docs/Create.md b/docs/Create.md index 6190ab2cdd9..80f2dac1ef6 100644 --- a/docs/Create.md +++ b/docs/Create.md @@ -50,6 +50,8 @@ const App = () => ( export default App; ``` +## Props + You can customize the `` component using the following props: * [`actions`](#actions): override the actions toolbar with a custom component @@ -599,3 +601,90 @@ export default OrderEdit; ``` **Tip:** If you'd like to avoid creating an intermediate component like ``, or are using an ``, you can use the [``](./Inputs.md#linking-two-inputs) component as an alternative. + +## Controlled Mode + +`` deduces the resource and the initial form values from the URL. This is fine for a creation page, but if you need to let users create records from another page, you probably want to define this parameter yourself. + +In that case, use the [`resource`](#resource) and [`record`](#record) props to set the creation parameters regardless of the URL. + +```jsx +import { Create, SimpleForm, TextInput, SelectInput } from "react-admin"; + +export const BookCreate = () => ( + + + + + + + +); +``` + +**Tip**: You probably also want to customize [the `redirect` prop](#redirect) if you embed an `` component in another page. + +## Headless Version + +Besides preparing a save handler, `` renders the default creation page layout (title, actions, a Material UI ``) and its children. If you need a custom creation layout, you may prefer [the `` component](./CreateBase.md), which only renders its children in a [`CreateContext`](./useCreateContext.md). + +```jsx +import { CreateBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookCreate = () => ( + + + + <Card> + <CardContent> + <SimpleForm> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> + </Card> + </Container> + </CreateBase> +); +``` + +In the previous example, `<SimpleForm>` grabs the save handler from the `CreateContext`. + +If you don't need the `CreateContext`, you can use [the `useCreateController` hook](./useCreateController.md), which does the same data fetching as `<CreateBase>` but lets you render the content. + +```jsx +import { useCreateController, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookCreate = () => { + const { save } = useCreateController(); + return ( + <Container> + <Title title="Create book" /> + <Card> + <CardContent> + <SimpleForm onSubmit={save}> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> + </Card> + </Container> + ); +}; +``` \ No newline at end of file diff --git a/docs/Edit.md b/docs/Edit.md index 3a12eaf1f51..436365b7b60 100644 --- a/docs/Edit.md +++ b/docs/Edit.md @@ -58,6 +58,8 @@ const App = () => ( export default App; ``` +## Props + You can customize the `<Edit>` component using the following props: * [`actions`](#actions): override the actions toolbar with a custom component @@ -770,3 +772,90 @@ export const PostEdit = () => ( ``` **Tips:** If you want users to be warned if they haven't pressed the Save button when they browse to another record, you can follow the tutorial [Navigating Through Records In`<Edit>` Views](./PrevNextButtons.md#navigating-through-records-in-edit-views-after-submit). + +## Controlled Mode + +`<Edit>` deduces the resource and the record id from the URL. This is fine for an edition page, but if you need to let users edit records from another page, you probably want to define the edit parameters yourself. + +In that case, use the [`resource`](#resource) and [`id`](#id) props to set the edit parameters regardless of the URL. + +```jsx +import { Edit, SimpleForm, TextInput, SelectInput } from "react-admin"; + +export const BookEdit = ({ id }) => ( + <Edit resource="books" id={id} redirect={false}> + <SimpleForm> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </Edit> +); +``` + +**Tip**: You probably also want to customize [the `redirect` prop](#redirect) if you embed an `<Edit>` component in another page. + +## Headless Version + +Besides fetching a record and preparing a save handler, `<Edit>` renders the default edition page layout (title, actions, a Material UI `<Card>`) and its children. If you need a custom edition layout, you may prefer [the `<EditBase>` component](./EditBase.md), which only renders its children in an [`EditContext`](./useEditContext.md). + +```jsx +import { EditBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookEdit = () => ( + <EditBase> + <Container> + <Title title="Book Edition" /> + <Card> + <CardContent> + <SimpleForm> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> + </Card> + </Container> + </EditBase> +); +``` + +In the previous example, `<SimpleForm>` grabs the record and the save handler from the `EditContext`. + +If you don't need the `EditContext`, you can use [the `useEditController` hook](./useEditController.md), which does the same data fetching as `<EditBase>` but lets you render the content. + +```tsx +import { useEditController, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookEdit = () => { + const { record, save } = useEditController(); + return ( + <Container> + <Title title={`Edit book ${record?.title}`} /> + <Card> + <CardContent> + <SimpleForm record={record} onSubmit={save}> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> + </Card> + </Container> + ); +}; +``` \ No newline at end of file diff --git a/docs/EditBase.md b/docs/EditBase.md index 33c39947ce5..5a731679f04 100644 --- a/docs/EditBase.md +++ b/docs/EditBase.md @@ -14,26 +14,27 @@ It does that by calling [`useEditController`](./useEditController.md), and by pu Use `<EditBase>` to create a custom Edition view, with exactly the content you add as child and nothing else (no title, Card, or list of actions as in the `<Edit>` component). ```jsx -import * as React from "react"; -import { EditBase, SimpleForm, TextInput, SelectInput } from "react-admin"; -import { Card } from "@mui/material"; +import { EditBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; export const BookEdit = () => ( <EditBase> - <div> + <Container> <Title title="Book Edition" /> <Card> - <SimpleForm> - <TextInput source="title" /> - <TextInput source="author" /> - <SelectInput source="availability" choices={[ - { id: "in_stock", name: "In stock" }, - { id: "out_of_stock", name: "Out of stock" }, - { id: "out_of_print", name: "Out of print" }, - ]} /> - </SimpleForm> + <CardContent> + <SimpleForm> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> </Card> - </div> + </Container> </EditBase> ); ``` diff --git a/docs/List.md b/docs/List.md index 855abcbd21f..08e0d25d9e4 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1081,3 +1081,152 @@ const ProductList = () => ( </List> ) ``` + +## Controlled Mode + +`<List>` deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself. + +In that case, use the [`resource`](#resource), [`sort`](#sort), [`filter`](#filter-permanent-filter), and [`perPage`](#perpage) props to set the list parameters. + +{% raw %} +```jsx +import { List, SimpleList } from 'react-admin'; +import { Container, Typography } from '@mui/material'; + +const Dashboard = () => ( + <Container> + <Typography>Latest posts</Typography> + <List + resource="posts" + sort={{ field: 'published_at', order: 'DESC' }} + filter={{ is_published: true }} + perPage={10} + > + <SimpleList + primaryText={record => record.title} + secondaryText={record => `${record.views} views`} + /> + </List> + <Typography>Latest comments</Typography> + <List + resource="comments" + sort={{ field: 'published_at', order: 'DESC' }} + perPage={10} + > + <SimpleList + primaryText={record => record.author.name} + secondaryText={record => record.body} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + /> + </List> + </Container> +) +``` +{% endraw %} + +If the `<List>` children allow to *modify* the list state (i.e. if they let users change the sort order, the filters, the selection, or the pagination), then you should also use the [`disableSyncWithLocation`](#disablesyncwithlocation) prop to prevent react-admin from changing the URL. This is the case e.g. if you use a `<Datagrid>`, which lets users sort the list by clicking on column headers. + +{% raw %} +```jsx +import { List, Datagrid, TextField, NumberField, DateField } from 'react-admin'; +import { Container, Typography } from '@mui/material'; + +const Dashboard = () => ( + <Container> + <Typography>Latest posts</Typography> + <List + resource="posts" + sort={{ field: 'published_at', order: 'DESC' }} + filter={{ is_published: true }} + perPage={10} + disableSyncWithLocation + > + <Datagrid bulkActionButtons={false}> + <TextField source="title" /> + <NumberField source="views" /> + </Datagrid> + </List> + <Typography>Latest comments</Typography> + <List + resource="comments" + sort={{ field: 'published_at', order: 'DESC' }} + perPage={10} + disableSyncWithLocation + > + <Datagrid bulkActionButtons={false}> + <TextField source="author.name" /> + <TextField source="body" /> + <DateField source="published_at" /> + </Datagrid> + </List> + </Container> +) +``` +{% endraw %} + +**Note**: If you render more than one `<Datagrid>` for the same resource in the same page, they will share the selection state (i.e. the checked checkboxes). This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. You can get rid of the checkboxes by setting `<Datagrid bulkActionButtons={false}>`. + +## Headless Version + +Besides fetching a list of records from the data provider, `<List>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, pagination) and its children. If you need a custom list layout, you may prefer [the `<ListBase>` component](./ListBase.md), which only renders its children in a [`ListContext`](./useListContext.md). + +```jsx +import { ListBase } from 'react-admin'; +import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; + +const ProductList = () => ( + <ListBase> + <Container> + <Typography variant="h4">All products</Typography> + <WithListContext render={({ isLoading, data }) => ( + !isLoading && ( + <Stack spacing={1}> + {data.map(product => ( + <Card key={product.id}> + <CardContent> + <Typography>{product.name}</Typography> + </CardContent> + </Card> + ))} + </Stack> + ) + )} /> + <WithListContext render={({ isLoading, total }) => ( + !isLoading && <Typography>{total} results</Typography> + )} /> + </Container> + </ListBase> +); +``` + +The previous example leverages [`<WithListContext>`](./WithListContext.md) to grab the data that `<ListBase>` stores in the `ListContext`. + +If you don't need the `ListContext`, you can use [the `useListController` hook](./useListController.md), which does the same data fetching as `<ListBase>` but lets you render the content. + +```jsx +import { useListController } from 'react-admin'; +import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; + +const ProductList = () => { + const { isLoading, data, total } = useListController(); + return ( + <Container> + <Typography variant="h4">All products</Typography> + {!isLoading && ( + <Stack spacing={1}> + {data.map(product => ( + <Card key={product.id}> + <CardContent> + <Typography>{product.name}</Typography> + </CardContent> + </Card> + ))} + </Stack> + )} + {!isLoading && <Typography>{total} results</Typography>} + </Container> + ); +}; +``` + +`useListController` returns callbacks to sort, filter, and paginate the list, so you can build a complete List page. Check [the `useListController`hook documentation](./useListController.md) for details. \ No newline at end of file diff --git a/docs/Show.md b/docs/Show.md index 72232a130dc..930a5d0dd38 100644 --- a/docs/Show.md +++ b/docs/Show.md @@ -604,31 +604,87 @@ export const PostShow = () => ( **Tips:** If you want the `<PrevNextButtons>` to link to the `<Show>` view, you have to set the `linkType` to `show`. See [the `<PrevNextButtons linkType>` prop](./PrevNextButtons.md#linktype). -## Headless Version +## Controlled Mode -The root component of `<Show>` is a Material UI `<Card>`. Besides, `<Show>` renders an action toolbar, and sets the page title. This may be useless if you have a completely custom layout. +`<show>` deduces the resource and the record id from the URL. This is fine for a detail page, but if you need to embed the details of a record in another page, you probably want to define these parameters yourself. -In that case, opt for [the `<ShowBase>` component](./ShowBase.md), a headless version of `<Show>`. +In that case, use the [`resource`](#resource) and [`id`](#id) props to set the show parameters regardless of the URL. ```jsx -// in src/posts.jsx -import { ShowBase } from 'react-admin'; +import { Show, SelectField, SimpleShowLayout, TextField, Title } from "react-admin"; -export const PostShow = () => ( +export const BookShow = ({ id }) => ( + <Show resource="books" id={id}> + <SimpleShowLayout> + <TextField source="title" /> + <TextField source="author" /> + <SelectField source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleShowLayout> + </Show> +); +``` + +## Headless Version + +Besides fetching a record, `<Show>` renders the default detail page layout (title, actions, a Material UI `<Card>`) and its children. If you need a custom detail layout, you may prefer [the `<ShowBase>` component](./ShowBase.md), which only renders its children in a [`ShowContext`](./useShowContext.md). + +```jsx +import { ShowBase, SelectField, SimpleShowLayout, TextField, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookShow = () => ( <ShowBase> - <Grid container> - <Grid item xs={8}> - <SimpleShowLayout> - ... - </SimpleShowLayout> - </Grid> - <Grid item xs={4}> - Show instructions... - </Grid> - </Grid> - <div> - Post related links... - </div> + <Container> + <Title title="Book Detail" /> + <Card> + <CardContent> + <SimpleShowLayout> + <TextField source="title" /> + <TextField source="author" /> + <SelectField source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleShowLayout> + </CardContent> + </Card> + </Container> </ShowBase> ); +``` + +In the previous example, `<SimpleShowLayout>` grabs the record from the `ShowContext`. + +If you don't need the `ShowContext`, you can use [the `useShowController` hook](./useShowController.md), which does the same data fetching as `<ShowBase>` but lets you render the content. + +```tsx +import { useShowController, SelectField, SimpleShowLayout, TextField, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; + +export const BookShow = () => { + const { record } = useShowController(); + return ( + <Container> + <Title title={`Edit book ${record?.title}`} /> + <Card> + <CardContent> + <SimpleShowLayout record={record}> + <TextField source="title" /> + <TextField source="author" /> + <SelectField source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleShowLayout> + </CardContent> + </Card> + </Container> + ); +}; ``` \ No newline at end of file diff --git a/docs/SimpleForm.md b/docs/SimpleForm.md index 603ce3953c6..d08bd279e2f 100644 --- a/docs/SimpleForm.md +++ b/docs/SimpleForm.md @@ -738,3 +738,35 @@ export default OrderEdit; ``` **Tip:** If you'd like to avoid creating an intermediate component like `<CityInput>`, or are using an `<ArrayInput>`, you can use the [`<FormDataConsumer>`](./Inputs.md#linking-two-inputs) component as an alternative. + +## Headless Version + +`<SimpleForm>` renders its children in a Material UI `<Stack>`, and renders a toolbar with a `<SaveButton>`. If you want to build a custom form layout, you can use [the `<Form>` component](./Form.md) instead. + +```jsx +import { Create, Form, TextInput, RichTextInput, SaveButton } from 'react-admin'; +import { Grid } from '@mui/material'; + +export const PostCreate = () => ( + <Create> + <Form> + <Grid container> + <Grid item xs={6}> + <TextInput source="title" fullWidth /> + </Grid> + <Grid item xs={6}> + <TextInput source="author" fullWidth /> + </Grid> + <Grid item xs={12}> + <RichTextInput source="body" fullWidth /> + </Grid> + <Grid item xs={12}> + <SaveButton /> + </Grid> + </Grid> + </Form> + </Create> +); +``` + +React-admin forms leverage react-hook-form's [`useForm` hook](https://react-hook-form.com/docs/useform). \ No newline at end of file diff --git a/docs/useCreateController.md b/docs/useCreateController.md index eaca03499ce..ed5da639805 100644 --- a/docs/useCreateController.md +++ b/docs/useCreateController.md @@ -13,36 +13,37 @@ React-admin calls `useCreateController` internally, when you use the `<Create>`, Use `useCreateController` to create a custom creation view, with exactly the content you need. -{% raw %} -```jsx -import * as React from "react"; -import { useCreateController, CreateContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin"; -import { Card } from "@mui/material"; +```tsx +import { useCreateController, SelectInput, SimpleForm, TextInput, Title } from "react-admin"; +import { Card, CardContent, Container } from "@mui/material"; export const BookCreate = () => { - const { save } = useCreateController({ resource: 'books' }); - return ( - <div> - <Title title="Book Creation" /> - <Card> - <SimpleForm onSubmit={save} record={{}} > - <TextInput source="title" /> - <TextInput source="author" /> - <SelectInput source="availability" choices={[ - { id: "in_stock", name: "In stock" }, - { id: "out_of_stock", name: "Out of stock" }, - { id: "out_of_print", name: "Out of print" }, - ]} /> - </SimpleForm> - </Card> - </div> - ); + const { save } = useCreateController(); + return ( + <Container> + <Title title="Create book" /> + <Card> + <CardContent> + <SimpleForm onSubmit={save}> + <TextInput source="title" /> + <TextInput source="author" /> + <SelectInput source="availability" choices={[ + { id: "in_stock", name: "In stock" }, + { id: "out_of_stock", name: "Out of stock" }, + { id: "out_of_print", name: "Out of print" }, + ]} /> + </SimpleForm> + </CardContent> + </Card> + </Container> + ); }; ``` -{% endraw %} **Tip**: If you just use the return value of `useCreateController` to put it in an `CreateContext`, use [the `<CreateBase>` component](./CreateBase.md) instead for simpler markup. +## Input Format + `useCreateController` accepts an options argument, with the following fields, all optional: * [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check @@ -52,6 +53,10 @@ export const BookCreate = () => { * [`resource`](./Create.md#resource): override the name of the resource to create * [`transform`](./Create.md#transform): transform the form data before calling `dataProvider.create()` +These fields are documented in [the `<Create>` component](./Create.md) documentation. + +## Return Value + `useCreateController` returns an object with the following fields: ```jsx diff --git a/docs/useEditController.md b/docs/useEditController.md index c8396e83dde..a091a208a3f 100644 --- a/docs/useEditController.md +++ b/docs/useEditController.md @@ -13,9 +13,7 @@ React-admin calls `useEditController` internally when you use the `<Edit>`, `<Ed Use `useEditController` to create a custom Edition view, with exactly the content you need. -{% raw %} ```jsx -import * as React from "react"; import { useParams } from "react-router-dom"; import { useEditController, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin"; import { Card } from "@mui/material"; @@ -42,10 +40,11 @@ export const BookEdit = () => { ); }; ``` -{% endraw %} **Tip**: If you just use the return value of `useEditController` to put it in an `EditContext`, use [the `<EditBase>` component](./EditBase.md) instead for simpler markup. +## Input Format + `useEditController` accepts an options argument, with the following fields, all optional: * [`disableAuthentication`](./Edit.md#disableauthentication): disable the authentication check @@ -57,6 +56,10 @@ export const BookEdit = () => { * [`resource`](./Edit.md#resource): override the name of the resource to create * [`transform`](./Edit.md#transform): transform the form data before calling `dataProvider.update()` +These fields are documented in [the `<Edit>` component](./Edit.md) documentation. + +## Return Value + `useEditController` returns an object with the following fields: ```jsx diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx index d5905a8971d..ae06a3b24ee 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx @@ -5,12 +5,13 @@ import { ResourceContextProvider, testDataProvider, } from 'ra-core'; +import { Stack, ThemeProvider, createTheme } from '@mui/material'; +import { MemoryRouter } from 'react-router-dom'; import { AdminContext } from '../AdminContext'; import { Edit } from '../detail'; import { NumberInput, TextInput } from '../input'; import { SimpleForm } from './SimpleForm'; -import { Stack } from '@mui/material'; export default { title: 'ra-ui-materialui/forms/SimpleForm' }; @@ -33,7 +34,7 @@ const Wrapper = ({ i18nProvider={i18nProvider} dataProvider={testDataProvider({ getOne: () => Promise.resolve({ data }), - })} + } as any)} > <ResourceContextProvider value="books"> <Edit id={1} sx={{ width: 600 }}> @@ -169,3 +170,22 @@ export const InputBasedValidation = () => ( </SimpleForm> </Wrapper> ); + +export const Controlled = () => { + const [record, setRecord] = React.useState({} as any); + return ( + <MemoryRouter> + <ThemeProvider theme={createTheme()}> + <SimpleForm + resource="books" + onSubmit={values => setRecord(values)} + > + <TextInput source="title" fullWidth /> + <TextInput source="author" /> + <NumberInput source="year" /> + </SimpleForm> + <div>Record values: {JSON.stringify(record)}</div> + </ThemeProvider> + </MemoryRouter> + ); +}; From 8dbece9bed9dc1305cf0c607df8a7b1508fd675f Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Wed, 15 Nov 2023 18:33:30 +0100 Subject: [PATCH 02/72] Add controlled and headless mode to InfiniteList --- docs/InfiniteList.md | 109 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/docs/InfiniteList.md b/docs/InfiniteList.md index f76e24955d7..b367d8c95ff 100644 --- a/docs/InfiniteList.md +++ b/docs/InfiniteList.md @@ -161,4 +161,111 @@ export const BookList = () => ( </InfiniteList> ); ``` -{% endraw %} \ No newline at end of file +{% endraw %} + +## Controlled Mode + +`<InfiniteList>` deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself. + +In that case, use the [`resource`](#resource), [`sort`](#sort), and [`filter`](#filter-permanent-filter) props to set the list parameters. + +{% raw %} +```jsx +import { InfiniteList, SimpleList } from 'react-admin'; +import { Container, Typography } from '@mui/material'; + +const Dashboard = () => ( + <Container> + <Typography>Latest posts</Typography> + <InfiniteList + resource="posts" + sort={{ field: 'published_at', order: 'DESC' }} + filter={{ is_published: true }} + disableSyncWithLocation + > + <SimpleList + primaryText={record => record.title} + secondaryText={record => `${record.views} views`} + /> + <InfinitePagination /> + </InfiniteList> + <Typography>Latest comments</Typography> + <InfiniteList + resource="comments" + sort={{ field: 'published_at', order: 'DESC' }} + perPage={10} + disableSyncWithLocation + > + <SimpleList + primaryText={record => record.author.name} + secondaryText={record => record.body} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + /> + <InfinitePagination /> + </InfiniteList> + </Container> +) +``` +{% endraw %} + +## Headless Version + +Besides fetching a list of records from the data provider, `<InfiniteList>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, infinite pagination) and its children. If you need a custom list layout, you may prefer the `<InfiniteListBase>` component, which only renders its children in a [`ListContext`](./useListContext.md). + +```jsx +import { InfiniteListBase, InfinitePagination } from 'react-admin'; +import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; + +const ProductList = () => ( + <InfiniteListBase> + <Container> + <Typography variant="h4">All products</Typography> + <WithListContext render={({ isLoading, data }) => ( + !isLoading && ( + <Stack spacing={1}> + {data.map(product => ( + <Card key={product.id}> + <CardContent> + <Typography>{product.name}</Typography> + </CardContent> + </Card> + ))} + </Stack> + ) + )} /> + <InfinitePagination /> + </Container> + </InfiniteListBase> +); +``` + +The previous example leverages [`<WithListContext>`](./WithListContext.md) to grab the data that `<ListBase>` stores in the `ListContext`. + +If you don't need the `ListContext`, you can use the `useInfiniteListController` hook, which does the same data fetching as `<InfiniteListBase>` but lets you render the content. + +```jsx +import { useInfiniteListController } from 'react-admin'; +import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; + +const ProductList = () => { + const { isLoading, data } = useInfiniteListController(); + return ( + <Container> + <Typography variant="h4">All products</Typography> + {!isLoading && ( + <Stack spacing={1}> + {data.map(product => ( + <Card key={product.id}> + <CardContent> + <Typography>{product.name}</Typography> + </CardContent> + </Card> + ))} + </Stack> + )} + </Container> + ); +}; +``` + +`useInfiniteListController` returns callbacks to sort, filter, and paginate the list, so you can build a complete List page. \ No newline at end of file From 49dbbdbdda704d1cee7016a7af6fa08b8323cfcf Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Wed, 15 Nov 2023 18:34:01 +0100 Subject: [PATCH 03/72] Mention Fields for relationships --- docs/List.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/List.md b/docs/List.md index 08e0d25d9e4..f9fb8f33c48 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1124,6 +1124,12 @@ const Dashboard = () => ( ``` {% endraw %} +**Note**: If you need to set the list parameters to render a list of records *related to another record*, there are better components than `<List>` for that. Check out the following components, specialized in fetching and displaying a list of related records: + +- [`<ReferenceArrayField>`](./ReferenceArrayField.md), +- [`<ReferenceManyField>`](./ReferenceManyField.md), +- [`<ReferenceManyToManyField>`](./ReferenceManyToManyField.md). + If the `<List>` children allow to *modify* the list state (i.e. if they let users change the sort order, the filters, the selection, or the pagination), then you should also use the [`disableSyncWithLocation`](#disablesyncwithlocation) prop to prevent react-admin from changing the URL. This is the case e.g. if you use a `<Datagrid>`, which lets users sort the list by clicking on column headers. {% raw %} From 8d4061dc40502b2e4aca8484d8fb7f9cb79e5054 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Wed, 15 Nov 2023 18:34:23 +0100 Subject: [PATCH 04/72] Mention that headless components can be used without MUI --- docs/CreateBase.md | 6 ++++-- docs/EditBase.md | 8 ++++++-- docs/Form.md | 4 +++- docs/ListBase.md | 6 ++++-- docs/ShowBase.md | 9 +++------ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/CreateBase.md b/docs/CreateBase.md index 73e9d1ce7f5..342c5d61b39 100644 --- a/docs/CreateBase.md +++ b/docs/CreateBase.md @@ -5,9 +5,11 @@ title: "The CreateBase Component" # `<CreateBase>` -The `<CreateBase>` component is a headless version of `<Create>`: it prepares a form submit handler, and renders its children. +`<CreateBase>` is a headless variant of [`<Create>`](./Create.md). It prepares a form submit handler, and renders its children in a [`CreateContext`](./useCreateContext.md). Use it to build a custom creation page layout, or to use another UI kit than Material UI. -It does that by calling `useCreateController`, and by putting the result in an `CreateContext`. +Contrary to [`<Create>`](./Create.md), it does not render the page layout, so no title, no actions, and no `<Card>`. + +`<CreateBase>` relies on the [`useCreateController`](./useCreateController.md) hook. ## Usage diff --git a/docs/EditBase.md b/docs/EditBase.md index 5a731679f04..9cac0f0738b 100644 --- a/docs/EditBase.md +++ b/docs/EditBase.md @@ -5,9 +5,11 @@ title: "The EditBase Component" # `<EditBase>` -The `<EditBase>` component is a headless version of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children. +`<EditBase>` is a headless variant of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children inside an [`EditContext`](./useEditContext.md). Use it to build a custom edition page layout, or to use another UI kit than Material UI. -It does that by calling [`useEditController`](./useEditController.md), and by putting the result in an `EditContext`. +Contrary to [`<Edit>`](./Edit.md), it does not render the page layout, so no title, no actions, and no `<Card>`. + +`<EditBase>` relies on the [`useEditController`](./useEditController.md) hook. ## Usage @@ -39,6 +41,8 @@ export const BookEdit = () => ( ); ``` +## Props + You can customize the `<EditBase>` component using the following props, documented in the `<Edit>` component: * `children`: the components that renders the form diff --git a/docs/Form.md b/docs/Form.md index feda9012628..3d0fb9feab7 100644 --- a/docs/Form.md +++ b/docs/Form.md @@ -5,10 +5,12 @@ title: "Form" # `<Form>` -The `<Form>` component creates a `<form>` to edit a record, and renders its children. It is a headless component used internally by `<SimpleForm>`, `<TabbedForm>`, and other form components. +`<Form>` is a headless component that creates a `<form>` to edit a record, and renders its children. Use it to build a custom form layout, or to use another UI kit than Material UI. `<Form>` reads the `record` from the `RecordContext`, uses it to initialize the defaultValues of a react-hook-form via `useForm`, turns the `validate` function info a react-hook-form compatible form validator, notifies the user when the input validation fails, and creates a form context via `<FormProvider>`. +`<Form>` is used internally by `<SimpleForm>`, `<TabbedForm>`, and other form components. + ## Usage Use `<Form>` to build completely custom form layouts. Don't forget to include a submit button (or react-admin's [`<SaveButton>`](./SaveButton.md)) to actually save the record. diff --git a/docs/ListBase.md b/docs/ListBase.md index c8cfea8972e..29f3c16eadb 100644 --- a/docs/ListBase.md +++ b/docs/ListBase.md @@ -5,9 +5,11 @@ title: "The ListBase Component" # `<ListBase>` -`<ListBase>` is a headless variant of `<List>`, as it does not render anything. `<ListBase>` calls [`useListController`](./useListController.md), puts the result in a `ListContext`, then renders its child. +`<ListBase>` is a headless variant of [`<List>`](./List.md). It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout, or to use another UI kit than Material UI. -If you want to display a record list in an entirely custom layout, i.e. use only the data fetching part of `<List>` and not the view layout, use `<ListBase>`. +Contrary to [`<List>`](./List.md), it does not render the page layout, so no title, no actions, no `<Card>`, and no pagination. + +`<ListBase>` relies on the [`useListController`](./useListController.md) hook. ## Usage diff --git a/docs/ShowBase.md b/docs/ShowBase.md index 4e129d87c3d..efd97caef20 100644 --- a/docs/ShowBase.md +++ b/docs/ShowBase.md @@ -5,15 +5,12 @@ title: "The ShowBase Component" # `<ShowBase>` -`<ShowBase>` is a headless component that lets you build custom Show pages. It handles the logic of the Show page: - -- it calls `useShowController` to fetch the record from the data provider via `dataProvider.getOne()`, -- it computes the default page title -- it creates a `ShowContext` and a `RecordContext`, -- it renders its child component +`<ShowBase>` is a headless variant of [`<Show>`](./Show.md). It fetches the record from the data provider via `dataProvider.getOne()`, puts it in a [`ShowContext`](./useShowContext.md), and renders its child. Use it to build a custom show page layout, or to use another UI kit than Material UI. Contrary to [`<Show>`](./Show.md), it does not render the page layout, so no title, no actions, and no `<Card>`. +`<ShowBase>` relies on the [`useShowController`](./useShowController.md) hook. + ## Usage Use `<ShowBase>` instead of `<Show>` when you want a completely custom page layout, without the default actions and title. From c911d196e6753ef28ee8bfd51c4b92f8db4db97f Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Thu, 16 Nov 2023 07:00:59 +0100 Subject: [PATCH 05/72] Update controllers introduction to mentino other ui kits --- docs/CreateBase.md | 2 +- docs/EditBase.md | 2 +- docs/ListBase.md | 2 +- docs/ShowBase.md | 2 +- docs/useCreateController.md | 7 +++++-- docs/useEditController.md | 6 ++++-- docs/useListController.md | 6 ++++-- docs/useShowController.md | 8 +++----- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/CreateBase.md b/docs/CreateBase.md index 342c5d61b39..c5c40d01bdf 100644 --- a/docs/CreateBase.md +++ b/docs/CreateBase.md @@ -5,7 +5,7 @@ title: "The CreateBase Component" # `<CreateBase>` -`<CreateBase>` is a headless variant of [`<Create>`](./Create.md). It prepares a form submit handler, and renders its children in a [`CreateContext`](./useCreateContext.md). Use it to build a custom creation page layout, or to use another UI kit than Material UI. +`<CreateBase>` is a headless variant of [`<Create>`](./Create.md). It prepares a form submit handler, and renders its children in a [`CreateContext`](./useCreateContext.md). Use it to build a custom creation page layout. Contrary to [`<Create>`](./Create.md), it does not render the page layout, so no title, no actions, and no `<Card>`. diff --git a/docs/EditBase.md b/docs/EditBase.md index 9cac0f0738b..e6965400ff8 100644 --- a/docs/EditBase.md +++ b/docs/EditBase.md @@ -5,7 +5,7 @@ title: "The EditBase Component" # `<EditBase>` -`<EditBase>` is a headless variant of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children inside an [`EditContext`](./useEditContext.md). Use it to build a custom edition page layout, or to use another UI kit than Material UI. +`<EditBase>` is a headless variant of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children inside an [`EditContext`](./useEditContext.md). Use it to build a custom edition page layout. Contrary to [`<Edit>`](./Edit.md), it does not render the page layout, so no title, no actions, and no `<Card>`. diff --git a/docs/ListBase.md b/docs/ListBase.md index 29f3c16eadb..4440b791a27 100644 --- a/docs/ListBase.md +++ b/docs/ListBase.md @@ -5,7 +5,7 @@ title: "The ListBase Component" # `<ListBase>` -`<ListBase>` is a headless variant of [`<List>`](./List.md). It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout, or to use another UI kit than Material UI. +`<ListBase>` is a headless variant of [`<List>`](./List.md). It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout. Contrary to [`<List>`](./List.md), it does not render the page layout, so no title, no actions, no `<Card>`, and no pagination. diff --git a/docs/ShowBase.md b/docs/ShowBase.md index efd97caef20..fc269411c55 100644 --- a/docs/ShowBase.md +++ b/docs/ShowBase.md @@ -5,7 +5,7 @@ title: "The ShowBase Component" # `<ShowBase>` -`<ShowBase>` is a headless variant of [`<Show>`](./Show.md). It fetches the record from the data provider via `dataProvider.getOne()`, puts it in a [`ShowContext`](./useShowContext.md), and renders its child. Use it to build a custom show page layout, or to use another UI kit than Material UI. +`<ShowBase>` is a headless variant of [`<Show>`](./Show.md). It fetches the record from the data provider via `dataProvider.getOne()`, puts it in a [`ShowContext`](./useShowContext.md), and renders its child. Use it to build a custom show page layout. Contrary to [`<Show>`](./Show.md), it does not render the page layout, so no title, no actions, and no `<Card>`. diff --git a/docs/useCreateController.md b/docs/useCreateController.md index ed5da639805..f1bdbee369a 100644 --- a/docs/useCreateController.md +++ b/docs/useCreateController.md @@ -5,9 +5,11 @@ title: "The useCreateController hook" # `useCreateController` -The `useCreateController` hook contains the logic of [the `<Create>` component](./Create.md): it prepares a form submit handler, and returns the data and callbacks necessary to render a Creation view. +`useCreateController` contains the headless logic of the [`<Create>`](./Create.md) component. It's useful to create a custom creation view. It's also the base hook when building a custom view with another UI kit than Material UI. -React-admin calls `useCreateController` internally, when you use the `<Create>`, or `<CreateBase>` component. +`useCreateController` reads the resource name from the resource context and browser location, computes the form default values, prepares a form submit handler based on `dataProvider.create()`, computes the default page title, and returns them. Its return value matches the [`CreateContext`](./useCreateContext.md) shape. + +`useCreateController` is used internally by [`<Create>`](./Create.md) and [`<CreateBase>`](./CreateBase.md). If your Create view uses react-admin components like `<SimpleForm>`, prefer [`<CreateBase>`](./CreateBase.md) to `useCreateController` as it takes care of creating a `<CreateContext>`. ## Usage @@ -62,6 +64,7 @@ These fields are documented in [the `<Create>` component](./Create.md) documenta ```jsx const { defaultTitle, // the translated title based on the resource, e.g. 'Create New Post' + record, // the default values of the creation form redirect, // the default redirection route. Defaults to 'list' resource, // the resource name, deduced from the location. e.g. 'posts' save, // the update callback, to be passed to the underlying form as submit handler diff --git a/docs/useEditController.md b/docs/useEditController.md index a091a208a3f..c2e7fc5cd62 100644 --- a/docs/useEditController.md +++ b/docs/useEditController.md @@ -5,9 +5,11 @@ title: "The useEditController hook" # `useEditController` -The `useEditController` hook contains the logic of [the `<Edit>` component](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and returns all the data and callbacks necessary to render an edition view. +`useEditController` contains the headless logic of the [`<Edit>`](./Edit.md) component. It's useful to create a custom edition view. It's also the base hook when building a custom view with another UI kit than Material UI. -React-admin calls `useEditController` internally when you use the `<Edit>`, `<EditBase>`, or `<EditGuesser>` component. +`useEditController` reads the resource name and id from the resource context and browser location, fetches the record via `dataProvider.getOne()` to initialize the form, prepares a form submit handler based on `dataProvider.update()`, computes the default page title, and returns them. Its return value matches the [`EditContext`](./useEditContext.md) shape. + +`useEditController` is used internally by [`<Edit>`](./Edit.md) and [`<EditBase>`](./EditBase.md). If your Edit view uses react-admin components like `<SimpleForm>`, prefer [`<EditBase>`](./EditBase.md) to `useEditController` as it takes care of creating a `<EditContext>`. ## Usage diff --git a/docs/useListController.md b/docs/useListController.md index 33d8a2a926a..57b9a227610 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -5,9 +5,11 @@ title: "useListController" # `useListController` -The `useListController` hook fetches the data, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them. Its return value match the `ListContext` shape. `useListController` is used internally by the `<List>` and `<ListBase>` components. +`useListController` contains the headless logic of the [`<List>`](./List.md) component. It's useful to create a custom List view. It's also the base hook when building a custom view with another UI kit than Material UI. -You can use it to create a custom List view, although its component counterpart, [`<ListBase>`](./ListBase.md), is probably better in most cases. +`useListController` reads the list parameters from the URL, calls `dataProvider.getList()`, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the [`ListContext`](./useListContext.md) shape. + +`useListController` is used internally by [`<List>`](./List.md) and [`<ListBase>`](./ListBase.md). If your list view uses react-admin components like `<Datagrid>`, prefer [`<ListBase>`](./ListBase.md) to `useListController` as it takes care of creating a `<ListContext>`. ## Usage diff --git a/docs/useShowController.md b/docs/useShowController.md index 5d5f183d858..3ec77114342 100644 --- a/docs/useShowController.md +++ b/docs/useShowController.md @@ -5,13 +5,11 @@ title: "useShowController" # `useShowController` -`useShowController` is the hook that handles all the controller logic for Show views. It's used by [`<Show>`](./Show.md) and [`<ShowBase>`](./ShowBase.md). +`useShowController` contains the headless logic of the [`<Show>`](./Show.md) component. It's useful to create a custom Show view. It's also the base hook when building a custom view with another UI kit than Material UI. -This hook takes care of three things: +`useShowController` reads the resource name and id from the resource context and browser location, fetches the record from the data provider via `dataProvider.getOne()`, computes the default page title, and returns them. Its return value matches the [`ShowContext`](./useShowContext.md) shape. -- it reads the resource name and id from the resource context and browser location -- it fetches the record from the data provider via `dataProvider.getOne()`, -- it computes the default page title +`useShowController` is used internally by [`<Show>`](./Show.md) and [`<ShowBase>`](./ShowBase.md). If your Show view uses react-admin components like `<TextField>`, prefer [`<ShowBase>`](./ShowBase.md) to `useShowController` as it takes care of creating a `<ShowContext>`. ## Usage From 5305cd18054e8d1d4c6008d925711bc2eb1d618b Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Thu, 16 Nov 2023 09:26:40 +0100 Subject: [PATCH 06/72] Add headless to features --- docs/Features.md | 87 +++++++++++++++++++++++++++++++++++ docs/img/list_ant_design.png | Bin 0 -> 82955 bytes 2 files changed, 87 insertions(+) create mode 100644 docs/img/list_ant_design.png diff --git a/docs/Features.md b/docs/Features.md index f15fac5dfd0..a3382906b1b 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -271,6 +271,93 @@ We have made many improvements to this default layout based on user feedback. In And for mobile users, react-admin renders a different layout with larger margins and less information density (see [Responsive](#responsive)). +## Headless + +React-admin components use Material UI components by default, which lets you scaffold a page in no time. However, the headless logic behind react-admin components is agnostic of the UI library, and is exposed via -Base components and controller hooks. + +This means you can use react-admin with any UI library you want - not only Material UI, but also [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. + +For instance, here a List view built [Ant Design](https://ant.design/): + +![List view built with Ant Design](./img/list_ant_design.png) + +It leverages the `useListController` hook: + +{% raw %} +```jsx +import { useListController } from 'react-admin'; +import { Card, Table, Button } from 'antd'; +import { + CheckCircleOutlined, + PlusOutlined, + EditOutlined, +} from '@ant-design/icons'; +import { Link } from 'react-router-dom'; + +const PostList = () => { + const { data, page, total, setPage, isLoading } = useListController({ + sort: { field: 'published_at', order: 'DESC' }, + perPage: 10, + }); + const handleTableChange = (pagination) => { + setPage(pagination.current); + }; + return ( + <> + <div style={{ margin: 10, textAlign: 'right' }}> + <Link to="/posts/create"> + <Button icon={<PlusOutlined />}>Create</Button> + </Link> + </div> + <Card bodyStyle={{ padding: '0' }} loading={isLoading}> + <Table + size="small" + dataSource={data} + columns={columns} + pagination={{ current: page, pageSize: 10, total }} + onChange={handleTableChange} + /> + </Card> + </> + ); +}; + +const columns = [ + { title: 'Id', dataIndex: 'id', key: 'id' }, + { title: 'Title', dataIndex: 'title', key: 'title' }, + { + title: 'Publication date', + dataIndex: 'published_at', + key: 'pub_at', + render: (value) => new Date(value).toLocaleDateString(), + }, + { + title: 'Commentable', + dataIndex: 'commentable', + key: 'commentable', + render: (value) => (value ? <CheckCircleOutlined /> : null), + }, + { + title: 'Actions', + render: (_, record) => ( + <Link to={`/posts/${record.id}`}> + <Button icon={<EditOutlined />}>Edit</Button> + </Link> + ), + }, +]; + +export default PostList; +``` +{% endraw %} + +Check the following hooks to learn more about headless controllers: + +- [`useListController`](./useListController.md) +- [`useEditController`](./useEditController.md) +- [`useCreateController`](./useCreateController.md) +- [`useShowController`](./useShowController.md) + ## Guessers & Scaffolding When mapping a new API route to a CRUD view, adding fields one by one can be tedious. React-admin provides a set of guessers that can automatically **generate a complete CRUD UI based on an API response**. diff --git a/docs/img/list_ant_design.png b/docs/img/list_ant_design.png new file mode 100644 index 0000000000000000000000000000000000000000..6812338722450d564babc565387f984125976975 GIT binary patch literal 82955 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%0Abb^hKVN<sNWMa+mW01#PnWx)mJ}=qLWi;c6Kr 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 literal 0 HcmV?d00001 From e256fab0ce15f62fb7d150b57566f18c941018a8 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Thu, 16 Nov 2023 09:33:12 +0100 Subject: [PATCH 07/72] better example for useListController --- docs/Features.md | 2 +- docs/useListController.md | 76 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/Features.md b/docs/Features.md index a3382906b1b..1d89e8805ac 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -277,7 +277,7 @@ React-admin components use Material UI components by default, which lets you sca This means you can use react-admin with any UI library you want - not only Material UI, but also [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. -For instance, here a List view built [Ant Design](https://ant.design/): +For instance, here a List view built with [Ant Design](https://ant.design/): ![List view built with Ant Design](./img/list_ant_design.png) diff --git a/docs/useListController.md b/docs/useListController.md index 57b9a227610..d02b21a7475 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -7,13 +7,87 @@ title: "useListController" `useListController` contains the headless logic of the [`<List>`](./List.md) component. It's useful to create a custom List view. It's also the base hook when building a custom view with another UI kit than Material UI. +![List view built with Ant Design](./img/list_ant_design.png) + `useListController` reads the list parameters from the URL, calls `dataProvider.getList()`, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the [`ListContext`](./useListContext.md) shape. `useListController` is used internally by [`<List>`](./List.md) and [`<ListBase>`](./ListBase.md). If your list view uses react-admin components like `<Datagrid>`, prefer [`<ListBase>`](./ListBase.md) to `useListController` as it takes care of creating a `<ListContext>`. ## Usage -It's common to call `useListController()` without parameters, and to put the result in a `ListContext` to make it available to the rest of the component tree. +`useListController` expects a parameters object defining the list sorting, pagination, and filters. It returns an object with the fetched data, and callbacks to modify the list parameters. + +Here the code for the post list view above, built with [Ant Design](https://ant.design/): + +{% raw %} +```jsx +import { useListController } from 'react-admin'; +import { Card, Table, Button } from 'antd'; +import { + CheckCircleOutlined, + PlusOutlined, + EditOutlined, +} from '@ant-design/icons'; +import { Link } from 'react-router-dom'; + +const PostList = () => { + const { data, page, total, setPage, isLoading } = useListController({ + sort: { field: 'published_at', order: 'DESC' }, + perPage: 10, + }); + const handleTableChange = (pagination) => { + setPage(pagination.current); + }; + return ( + <> + <div style={{ margin: 10, textAlign: 'right' }}> + <Link to="/posts/create"> + <Button icon={<PlusOutlined />}>Create</Button> + </Link> + </div> + <Card bodyStyle={{ padding: '0' }} loading={isLoading}> + <Table + size="small" + dataSource={data} + columns={columns} + pagination={{ current: page, pageSize: 10, total }} + onChange={handleTableChange} + /> + </Card> + </> + ); +}; + +const columns = [ + { title: 'Id', dataIndex: 'id', key: 'id' }, + { title: 'Title', dataIndex: 'title', key: 'title' }, + { + title: 'Publication date', + dataIndex: 'published_at', + key: 'pub_at', + render: (value) => new Date(value).toLocaleDateString(), + }, + { + title: 'Commentable', + dataIndex: 'commentable', + key: 'commentable', + render: (value) => (value ? <CheckCircleOutlined /> : null), + }, + { + title: 'Actions', + render: (_, record) => ( + <Link to={`/posts/${record.id}`}> + <Button icon={<EditOutlined />}>Edit</Button> + </Link> + ), + }, +]; + +export default PostList; +``` +{% endraw %} + +When using react-admin components, it's common to call `useListController()` without parameters, and to put the result in a `ListContext` to make it available to the rest of the component tree. ```jsx import { From 45481b18afe36896adabc60f4442e9b1439203cd Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 17 Nov 2023 15:30:33 +0100 Subject: [PATCH 08/72] changelog for v4.16.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb326e65cd..95e5a52b4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v4.16.0 + +* Add `<SingleFieldList empty gap direction>` props, and allow it to be used without `children` ([#9439](https://github.com/marmelab/react-admin/pull/9439)) ([fzaninotto](https://github.com/fzaninotto)) +* Add `<LoadingIndicator onClick>` prop, allowing to trigger actions (like a refresh) on click ([#9420](https://github.com/marmelab/react-admin/pull/9420)) ([david-bezero](https://github.com/david-bezero)) +* Add `<LocalesMenuButton icon>` prop to customize the locales button icon ([#9380](https://github.com/marmelab/react-admin/pull/9380)) ([djhi](https://github.com/djhi)) +* Add `<Form disableInvalidFormNotification>` to allow disabling notifications when the form is invalid ([#9353](https://github.com/marmelab/react-admin/pull/9353)) ([tim-hoffmann](https://github.com/tim-hoffmann)) + + ## v4.15.5 * Add support for `fetchOptions` to `<CheckForApplicationUpdate>` ([#9436](https://github.com/marmelab/react-admin/pull/9436)) ([smeng9](https://github.com/smeng9)) From f046aac1d4331ce87ca190cb69544a0cdbc63790 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 17 Nov 2023 15:31:47 +0100 Subject: [PATCH 09/72] v4.16.0 --- examples/data-generator/package.json | 4 ++-- examples/simple/package.json | 14 +++++++------- lerna.json | 2 +- packages/create-react-admin/package.json | 2 +- packages/ra-core/package.json | 2 +- packages/ra-data-fakerest/package.json | 4 ++-- packages/ra-data-graphql-simple/package.json | 4 ++-- packages/ra-data-graphql/package.json | 2 +- packages/ra-data-json-server/package.json | 4 ++-- packages/ra-data-localforage/package.json | 4 ++-- packages/ra-data-localstorage/package.json | 4 ++-- packages/ra-data-simple-rest/package.json | 4 ++-- packages/ra-i18n-i18next/package.json | 4 ++-- packages/ra-i18n-polyglot/package.json | 4 ++-- packages/ra-input-rich-text/package.json | 10 +++++----- packages/ra-language-english/package.json | 4 ++-- packages/ra-language-french/package.json | 4 ++-- packages/ra-no-code/package.json | 6 +++--- packages/ra-ui-materialui/package.json | 8 ++++---- packages/react-admin/package.json | 10 +++++----- 20 files changed, 50 insertions(+), 50 deletions(-) diff --git a/examples/data-generator/package.json b/examples/data-generator/package.json index e3c847bb919..332f6884de1 100644 --- a/examples/data-generator/package.json +++ b/examples/data-generator/package.json @@ -1,6 +1,6 @@ { "name": "data-generator-retail", - "version": "4.15.5", + "version": "4.16.0", "homepage": "https://github.com/marmelab/react-admin/tree/master/examples/data-generator", "bugs": "https://github.com/marmelab/react-admin/issues", "license": "MIT", @@ -19,7 +19,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.15.5", + "ra-core": "^4.16.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/examples/simple/package.json b/examples/simple/package.json index 2253d438149..d176cfbc8a1 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -1,6 +1,6 @@ { "name": "simple", - "version": "4.15.5", + "version": "4.16.0", "private": true, "scripts": { "dev": "vite", @@ -17,13 +17,13 @@ "lodash": "~4.17.5", "prop-types": "^15.7.2", "proxy-polyfill": "^0.3.0", - "ra-data-fakerest": "^4.15.5", - "ra-i18n-polyglot": "^4.15.5", - "ra-input-rich-text": "^4.15.5", - "ra-language-english": "^4.15.5", - "ra-language-french": "^4.15.5", + "ra-data-fakerest": "^4.16.0", + "ra-i18n-polyglot": "^4.16.0", + "ra-input-rich-text": "^4.16.0", + "ra-language-english": "^4.16.0", + "ra-language-french": "^4.16.0", "react": "^17.0.0", - "react-admin": "^4.15.5", + "react-admin": "^4.16.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-query": "^3.32.1", diff --git a/lerna.json b/lerna.json index 0a2192f0343..277d7360c11 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "lerna": "2.5.1", "packages": ["examples/data-generator", "examples/simple", "packages/*"], - "version": "4.15.5" + "version": "4.16.0" } diff --git a/packages/create-react-admin/package.json b/packages/create-react-admin/package.json index bfd149190e1..7a37ff1b4c3 100644 --- a/packages/create-react-admin/package.json +++ b/packages/create-react-admin/package.json @@ -1,7 +1,7 @@ { "name": "create-react-admin", "description": "A CLI to quickly start a new react-admin project", - "version": "4.15.5", + "version": "4.16.0", "license": "MIT", "bin": "lib/cli.js", "authors": [ diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index f82e03328cc..c068d742fe1 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "4.15.5", + "version": "4.16.0", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", diff --git a/packages/ra-data-fakerest/package.json b/packages/ra-data-fakerest/package.json index 7be7e0822ea..aa4f2a3230d 100644 --- a/packages/ra-data-fakerest/package.json +++ b/packages/ra-data-fakerest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-fakerest", - "version": "4.15.5", + "version": "4.16.0", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -43,7 +43,7 @@ "@types/jest": "^29.5.2", "cross-env": "^5.2.0", "expect": "^27.4.6", - "ra-core": "^4.15.5", + "ra-core": "^4.16.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index 45d90a86cf9..d152f0d5b2f 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql-simple", - "version": "4.15.5", + "version": "4.16.0", "description": "A GraphQL simple data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -37,7 +37,7 @@ "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.17.5", "pluralize": "~7.0.0", - "ra-data-graphql": "^4.15.5" + "ra-data-graphql": "^4.16.0" }, "peerDependencies": { "graphql": "^15.6.0", diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index 0cc0f2752b6..08ea50a493b 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql", - "version": "4.15.5", + "version": "4.16.0", "description": "A GraphQL data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/ra-data-json-server/package.json b/packages/ra-data-json-server/package.json index 2e707da9ecf..390c78597d7 100644 --- a/packages/ra-data-json-server/package.json +++ b/packages/ra-data-json-server/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "4.15.5", + "version": "4.16.0", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "^7.1.1", - "ra-core": "^4.15.5" + "ra-core": "^4.16.0" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localforage/package.json b/packages/ra-data-localforage/package.json index 78402a97eb8..b3f0a37844a 100644 --- a/packages/ra-data-localforage/package.json +++ b/packages/ra-data-localforage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-forage", - "version": "4.15.5", + "version": "4.16.0", "description": "LocalForage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -42,7 +42,7 @@ "dependencies": { "localforage": "^1.7.1", "lodash": "~4.17.5", - "ra-data-fakerest": "^4.15.5" + "ra-data-fakerest": "^4.16.0" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localstorage/package.json b/packages/ra-data-localstorage/package.json index 881506da4f1..a90dadef5a6 100644 --- a/packages/ra-data-localstorage/package.json +++ b/packages/ra-data-localstorage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-storage", - "version": "4.15.5", + "version": "4.16.0", "description": "Local storage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -38,7 +38,7 @@ }, "dependencies": { "lodash": "~4.17.5", - "ra-data-fakerest": "^4.15.5" + "ra-data-fakerest": "^4.16.0" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-simple-rest/package.json b/packages/ra-data-simple-rest/package.json index b5752c50bb5..94470d459bd 100644 --- a/packages/ra-data-simple-rest/package.json +++ b/packages/ra-data-simple-rest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "4.15.5", + "version": "4.16.0", "description": "Simple REST data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -29,7 +29,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.15.5", + "ra-core": "^4.16.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json index 2baf3b89144..9a46ceb9999 100644 --- a/packages/ra-i18n-i18next/package.json +++ b/packages/ra-i18n-i18next/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-i18next", - "version": "4.15.5", + "version": "4.16.0", "description": "i18next i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "i18next": "^23.5.1", - "ra-core": "^4.15.5", + "ra-core": "^4.16.0", "react-i18next": "^13.2.2" }, "devDependencies": { diff --git a/packages/ra-i18n-polyglot/package.json b/packages/ra-i18n-polyglot/package.json index 916bb213e1e..71eb90bdc4e 100644 --- a/packages/ra-i18n-polyglot/package.json +++ b/packages/ra-i18n-polyglot/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-polyglot", - "version": "4.15.5", + "version": "4.16.0", "description": "Polyglot i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "node-polyglot": "^2.2.2", - "ra-core": "^4.15.5" + "ra-core": "^4.16.0" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-input-rich-text/package.json b/packages/ra-input-rich-text/package.json index fc405fbd8af..97b35476a77 100644 --- a/packages/ra-input-rich-text/package.json +++ b/packages/ra-input-rich-text/package.json @@ -1,6 +1,6 @@ { "name": "ra-input-rich-text", - "version": "4.15.5", + "version": "4.16.0", "description": "<RichTextInput> component for react-admin, useful for editing HTML code in admin GUIs.", "author": "Gildas Garcia", "repository": "marmelab/react-admin", @@ -51,10 +51,10 @@ "@testing-library/react": "^11.2.3", "@tiptap/extension-mention": "^2.0.3", "@tiptap/suggestion": "^2.0.3", - "data-generator-retail": "^4.15.5", - "ra-core": "^4.15.5", - "ra-data-fakerest": "^4.15.5", - "ra-ui-materialui": "^4.15.5", + "data-generator-retail": "^4.16.0", + "ra-core": "^4.16.0", + "ra-data-fakerest": "^4.16.0", + "ra-ui-materialui": "^4.16.0", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/ra-language-english/package.json b/packages/ra-language-english/package.json index f319cd4656c..21fbe26001c 100644 --- a/packages/ra-language-english/package.json +++ b/packages/ra-language-english/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-english", - "version": "4.15.5", + "version": "4.16.0", "description": "English messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.15.5" + "ra-core": "^4.16.0" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-language-french/package.json b/packages/ra-language-french/package.json index e02db6591d4..bd859a17433 100644 --- a/packages/ra-language-french/package.json +++ b/packages/ra-language-french/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-french", - "version": "4.15.5", + "version": "4.16.0", "description": "French messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.15.5" + "ra-core": "^4.16.0" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index 3c3b3d4f42d..c1e649d922d 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -1,6 +1,6 @@ { "name": "ra-no-code", - "version": "4.15.5", + "version": "4.16.0", "description": "", "files": [ "*.md", @@ -48,8 +48,8 @@ "lodash": "~4.17.5", "papaparse": "^5.3.0", "prop-types": "^15.6.1", - "ra-data-local-storage": "^4.15.5", - "react-admin": "^4.15.5", + "ra-data-local-storage": "^4.16.0", + "react-admin": "^4.16.0", "react-dropzone": "^12.0.4", "react-query": "^3.32.1" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 286294284f4..3ca5ef397f9 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "4.15.5", + "version": "4.16.0", "description": "UI Components for react-admin with Material UI", "files": [ "*.md", @@ -35,9 +35,9 @@ "file-api": "~0.10.4", "history": "^5.1.0", "ignore-styles": "~5.0.1", - "ra-core": "^4.15.5", - "ra-i18n-polyglot": "^4.15.5", - "ra-language-english": "^4.15.5", + "ra-core": "^4.16.0", + "ra-i18n-polyglot": "^4.16.0", + "ra-language-english": "^4.16.0", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 3f4b240f5c3..f1a9b3dce09 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "4.15.5", + "version": "4.16.0", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -41,10 +41,10 @@ "@mui/icons-material": "^5.0.1", "@mui/material": "^5.0.2", "history": "^5.1.0", - "ra-core": "^4.15.5", - "ra-i18n-polyglot": "^4.15.5", - "ra-language-english": "^4.15.5", - "ra-ui-materialui": "^4.15.5", + "ra-core": "^4.16.0", + "ra-i18n-polyglot": "^4.16.0", + "ra-language-english": "^4.16.0", + "ra-ui-materialui": "^4.16.0", "react-hook-form": "^7.43.9", "react-router": "^6.1.0", "react-router-dom": "^6.1.0" From 8efcae2f989ef3b9798e54038e3f96232bfa5cba Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 17 Nov 2023 15:33:07 +0100 Subject: [PATCH 10/72] update yarn lock --- yarn.lock | 82 +++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8486abaaec0..5c016c48f36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9720,14 +9720,14 @@ __metadata: languageName: node linkType: hard -"data-generator-retail@^4.12.0, data-generator-retail@^4.15.5, data-generator-retail@workspace:examples/data-generator": +"data-generator-retail@^4.12.0, data-generator-retail@^4.16.0, data-generator-retail@workspace:examples/data-generator": version: 0.0.0-use.local resolution: "data-generator-retail@workspace:examples/data-generator" dependencies: cross-env: ^5.2.0 date-fns: ^2.19.0 faker: ^4.1.0 - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18086,7 +18086,7 @@ __metadata: languageName: node linkType: hard -"ra-core@^4.15.5, ra-core@workspace:packages/ra-core": +"ra-core@^4.16.0, ra-core@workspace:packages/ra-core": version: 0.0.0-use.local resolution: "ra-core@workspace:packages/ra-core" dependencies: @@ -18132,7 +18132,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.15.5, ra-data-fakerest@workspace:packages/ra-data-fakerest": +"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.16.0, ra-data-fakerest@workspace:packages/ra-data-fakerest": version: 0.0.0-use.local resolution: "ra-data-fakerest@workspace:packages/ra-data-fakerest" dependencies: @@ -18140,7 +18140,7 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 fakerest: ^3.0.0 - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18158,7 +18158,7 @@ __metadata: graphql-ast-types-browser: ~1.0.2 lodash: ~4.17.5 pluralize: ~7.0.0 - ra-data-graphql: ^4.15.5 + ra-data-graphql: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18167,7 +18167,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-graphql@^4.12.0, ra-data-graphql@^4.15.5, ra-data-graphql@workspace:packages/ra-data-graphql": +"ra-data-graphql@^4.12.0, ra-data-graphql@^4.16.0, ra-data-graphql@workspace:packages/ra-data-graphql": version: 0.0.0-use.local resolution: "ra-data-graphql@workspace:packages/ra-data-graphql" dependencies: @@ -18190,7 +18190,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18203,7 +18203,7 @@ __metadata: cross-env: ^5.2.0 localforage: ^1.7.1 lodash: ~4.17.5 - ra-data-fakerest: ^4.15.5 + ra-data-fakerest: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18211,13 +18211,13 @@ __metadata: languageName: unknown linkType: soft -"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.15.5, ra-data-local-storage@workspace:packages/ra-data-localstorage": +"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.16.0, ra-data-local-storage@workspace:packages/ra-data-localstorage": version: 0.0.0-use.local resolution: "ra-data-local-storage@workspace:packages/ra-data-localstorage" dependencies: cross-env: ^5.2.0 lodash: ~4.17.5 - ra-data-fakerest: ^4.15.5 + ra-data-fakerest: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18231,7 +18231,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18246,26 +18246,26 @@ __metadata: cross-env: ^5.2.0 i18next: ^23.5.1 i18next-resources-to-backend: ^1.1.4 - ra-core: ^4.15.5 + ra-core: ^4.16.0 react-i18next: ^13.2.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.15.5, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": +"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.16.0, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": version: 0.0.0-use.local resolution: "ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot" dependencies: cross-env: ^5.2.0 node-polyglot: ^2.2.2 - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.15.5, ra-input-rich-text@workspace:packages/ra-input-rich-text": +"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.16.0, ra-input-rich-text@workspace:packages/ra-input-rich-text": version: 0.0.0-use.local resolution: "ra-input-rich-text@workspace:packages/ra-input-rich-text" dependencies: @@ -18287,10 +18287,10 @@ __metadata: "@tiptap/starter-kit": ^2.0.3 "@tiptap/suggestion": ^2.0.3 clsx: ^1.1.1 - data-generator-retail: ^4.15.5 - ra-core: ^4.15.5 - ra-data-fakerest: ^4.15.5 - ra-ui-materialui: ^4.15.5 + data-generator-retail: ^4.16.0 + ra-core: ^4.16.0 + ra-data-fakerest: ^4.16.0 + ra-ui-materialui: ^4.16.0 react: ^17.0.0 react-dom: ^17.0.0 react-hook-form: ^7.43.9 @@ -18307,21 +18307,21 @@ __metadata: languageName: unknown linkType: soft -"ra-language-english@^4.12.0, ra-language-english@^4.15.5, ra-language-english@workspace:packages/ra-language-english": +"ra-language-english@^4.12.0, ra-language-english@^4.16.0, ra-language-english@workspace:packages/ra-language-english": version: 0.0.0-use.local resolution: "ra-language-english@workspace:packages/ra-language-english" dependencies: - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-language-french@^4.12.0, ra-language-french@^4.15.5, ra-language-french@workspace:packages/ra-language-french": +"ra-language-french@^4.12.0, ra-language-french@^4.16.0, ra-language-french@workspace:packages/ra-language-french": version: 0.0.0-use.local resolution: "ra-language-french@workspace:packages/ra-language-french" dependencies: - ra-core: ^4.15.5 + ra-core: ^4.16.0 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18342,9 +18342,9 @@ __metadata: lodash: ~4.17.5 papaparse: ^5.3.0 prop-types: ^15.6.1 - ra-data-local-storage: ^4.15.5 + ra-data-local-storage: ^4.16.0 react: ^17.0.0 - react-admin: ^4.15.5 + react-admin: ^4.16.0 react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 @@ -18358,7 +18358,7 @@ __metadata: languageName: unknown linkType: soft -"ra-ui-materialui@^4.15.5, ra-ui-materialui@workspace:packages/ra-ui-materialui": +"ra-ui-materialui@^4.16.0, ra-ui-materialui@workspace:packages/ra-ui-materialui": version: 0.0.0-use.local resolution: "ra-ui-materialui@workspace:packages/ra-ui-materialui" dependencies: @@ -18381,9 +18381,9 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.0 query-string: ^7.1.1 - ra-core: ^4.15.5 - ra-i18n-polyglot: ^4.15.5 - ra-language-english: ^4.15.5 + ra-core: ^4.16.0 + ra-i18n-polyglot: ^4.16.0 + ra-language-english: ^4.16.0 react: ^17.0.0 react-dom: ^17.0.0 react-dropzone: ^12.0.4 @@ -18561,7 +18561,7 @@ __metadata: languageName: unknown linkType: soft -"react-admin@^4.12.0, react-admin@^4.15.5, react-admin@workspace:packages/react-admin": +"react-admin@^4.12.0, react-admin@^4.16.0, react-admin@workspace:packages/react-admin": version: 0.0.0-use.local resolution: "react-admin@workspace:packages/react-admin" dependencies: @@ -18572,10 +18572,10 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 history: ^5.1.0 - ra-core: ^4.15.5 - ra-i18n-polyglot: ^4.15.5 - ra-language-english: ^4.15.5 - ra-ui-materialui: ^4.15.5 + ra-core: ^4.16.0 + ra-i18n-polyglot: ^4.16.0 + ra-language-english: ^4.16.0 + ra-ui-materialui: ^4.16.0 react-hook-form: ^7.43.9 react-router: ^6.1.0 react-router-dom: ^6.1.0 @@ -20135,13 +20135,13 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.2 proxy-polyfill: ^0.3.0 - ra-data-fakerest: ^4.15.5 - ra-i18n-polyglot: ^4.15.5 - ra-input-rich-text: ^4.15.5 - ra-language-english: ^4.15.5 - ra-language-french: ^4.15.5 + ra-data-fakerest: ^4.16.0 + ra-i18n-polyglot: ^4.16.0 + ra-input-rich-text: ^4.16.0 + ra-language-english: ^4.16.0 + ra-language-french: ^4.16.0 react: ^17.0.0 - react-admin: ^4.15.5 + react-admin: ^4.16.0 react-app-polyfill: ^1.0.4 react-dom: ^17.0.0 react-hook-form: ^7.43.9 From 43c27d3d2072aadd0b8e9574a5ec054447ed1f64 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 17 Nov 2023 15:35:04 +0100 Subject: [PATCH 11/72] update create-react-admin to 4.16 --- packages/create-react-admin/src/generateProject.ts | 2 +- .../create-react-admin/templates/ra-data-fakerest/package.json | 2 +- .../templates/ra-data-json-server/package.json | 2 +- .../templates/ra-data-simple-rest/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/create-react-admin/src/generateProject.ts b/packages/create-react-admin/src/generateProject.ts index 7913e2bbf1f..4e72d0370d6 100644 --- a/packages/create-react-admin/src/generateProject.ts +++ b/packages/create-react-admin/src/generateProject.ts @@ -191,7 +191,7 @@ const BasePackageJson = { }, dependencies: { react: '^18.2.0', - 'react-admin': '^4.14.0', + 'react-admin': '^4.16.0', 'react-dom': '^18.2.0', }, devDependencies: { diff --git a/packages/create-react-admin/templates/ra-data-fakerest/package.json b/packages/create-react-admin/templates/ra-data-fakerest/package.json index 3485c59f584..505ce583884 100644 --- a/packages/create-react-admin/templates/ra-data-fakerest/package.json +++ b/packages/create-react-admin/templates/ra-data-fakerest/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "ra-data-fakerest": "^4.14.0" + "ra-data-fakerest": "^4.16.0" } } diff --git a/packages/create-react-admin/templates/ra-data-json-server/package.json b/packages/create-react-admin/templates/ra-data-json-server/package.json index bc483a2c452..78ed680e9bd 100644 --- a/packages/create-react-admin/templates/ra-data-json-server/package.json +++ b/packages/create-react-admin/templates/ra-data-json-server/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "ra-data-json-server": "^4.14.0" + "ra-data-json-server": "^4.16.0" } } diff --git a/packages/create-react-admin/templates/ra-data-simple-rest/package.json b/packages/create-react-admin/templates/ra-data-simple-rest/package.json index 6568cebd94f..fa5ca66364c 100644 --- a/packages/create-react-admin/templates/ra-data-simple-rest/package.json +++ b/packages/create-react-admin/templates/ra-data-simple-rest/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "ra-data-simple-rest": "^4.14.0" + "ra-data-simple-rest": "^4.16.0" } } From f4e567387fde696a37c0e2524be4e05643695b25 Mon Sep 17 00:00:00 2001 From: adrien guernier <adrien@marmelab.com> Date: Fri, 17 Nov 2023 15:54:22 +0100 Subject: [PATCH 12/72] Fix: reorder Discord and Github icons to match the website order --- docs/_includes/nav.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html index 76ac755e46a..78cc25412af 100644 --- a/docs/_includes/nav.html +++ b/docs/_includes/nav.html @@ -27,11 +27,12 @@ > </li> <li> - <a href="https://discord.gg/GeZF9sqh3N" title="Discord"> + <a + href="https://github.com/marmelab/react-admin" + title="GitHub" + > <svg - height="25px" - width="25px" - viewBox="0 -28.5 256 256" + viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style=" height: 20px; @@ -40,18 +41,17 @@ " > <path - d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" + d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403z" ></path> </svg> </a> </li> <li> - <a - href="https://github.com/marmelab/react-admin" - title="GitHub" - > + <a href="https://discord.gg/GeZF9sqh3N" title="Discord"> <svg - viewBox="0 0 256 250" + height="25px" + width="25px" + viewBox="0 -28.5 256 256" preserveAspectRatio="xMidYMid" style=" height: 20px; @@ -61,7 +61,7 @@ " > <path - d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403z" + d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" ></path> </svg> </a> From a31e2e12daadc5c124431742f691be4866fac18d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 17 Nov 2023 16:16:47 +0100 Subject: [PATCH 13/72] [Doc] Fix RBAC doc markup and unwanted scrollbar --- docs/AuthRBAC.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/AuthRBAC.md b/docs/AuthRBAC.md index cf82e581839..6f37cd27ead 100644 --- a/docs/AuthRBAC.md +++ b/docs/AuthRBAC.md @@ -7,7 +7,7 @@ title: "RBAC" React-admin Enterprise Edition contains [the ra-rbac module](https://marmelab.com/ra-enterprise/modules/ra-rbac)<img class="icon" src="./img/premium.svg" />, which adds fine-grained permissions to your admin. This module extends the `authProvider` and adds replacement for many react-admin components that use these permissions. -<video controls="controls" style="max-width: 100%"> +<video controls="controls" style="max-width: 96%"> <source src="./img/ra-rbac.mp4" type="video/mp4" /> </video> @@ -905,10 +905,9 @@ For instance, to allow users access to the following tab `<TabbedShowLayout.Tab ## `<TabbedForm>` -<TabbedForm> shows only the tabs for which users have write permissions, using the `[resource].tab.[tabName]` string as resource identifier. It also renders the delete button only if the user has a permission for the `delete` action in the current resource. `<TabbedForm.Tab>` shows only the child inputs for which users have the write permissions, using the `[resource].[source]` string as resource identifier. +`<TabbedForm>` shows only the tabs for which users have write permissions, using the `[resource].tab.[tabName]` string as resource identifier. It also renders the delete button only if the user has a permission for the `delete` action in the current resource. `<TabbedForm.Tab>` shows only the child inputs for which users have the write permissions, using the `[resource].[source]` string as resource identifier. -```jsx ```tsx import { Edit, TextInput } from 'react-admin'; import { TabbedForm } from '@react-admin/ra-rbac'; @@ -953,7 +952,6 @@ const ProductEdit = () => ( </Edit> ); ``` -``` You must add a `name` prop to the `<TabbedForm.Tab>` so you can reference it in the permissions. Then, to allow users to access a particular `<TabbedForm.Tab>`, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.tab.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the `<TabbedForm.Tab>`. From 000bec3cf2e5367beb448bd6d6a6bd0bfcb86c08 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Mon, 20 Nov 2023 11:15:09 +0100 Subject: [PATCH 14/72] Fix `<FileInput>` should display a validation errors right away when form mode is 'onChange' --- .../src/input/FileInput.spec.tsx | 102 +++++++++++++++++- .../ra-ui-materialui/src/input/FileInput.tsx | 6 +- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/FileInput.spec.tsx b/packages/ra-ui-materialui/src/input/FileInput.spec.tsx index 7a384e0fea8..8282038e1fb 100644 --- a/packages/ra-ui-materialui/src/input/FileInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/FileInput.spec.tsx @@ -6,12 +6,13 @@ import { waitFor, waitForElementToBeRemoved, } from '@testing-library/react'; -import { testDataProvider } from 'ra-core'; +import { required, testDataProvider } from 'ra-core'; import { AdminContext } from '../AdminContext'; import { SimpleForm, Toolbar } from '../form'; import { FileField, ImageField } from '../field'; import { FileInput } from './FileInput'; +import { TextInput } from './TextInput'; import { SaveButton } from '../button'; describe('<FileInput />', () => { @@ -469,6 +470,105 @@ describe('<FileInput />', () => { test(<CustomLabel />, 'Custom label in component'); }); + describe('Validation', () => { + it('should display a validation error if the value is required and there is no file', async () => { + const onSubmit = jest.fn(); + + render( + <AdminContext dataProvider={testDataProvider()}> + <SimpleForm onSubmit={onSubmit}> + <FileInput + {...defaultPropsMultiple} + validate={required()} + > + <FileField source="src" title="title" /> + </FileInput> + <TextInput source="title" resource="posts" /> + </SimpleForm> + </AdminContext> + ); + + fireEvent.change( + await screen.findByLabelText('resources.posts.fields.title'), + { + target: { value: 'Hello world!' }, + } + ); + fireEvent.click(screen.getByLabelText('ra.action.save')); + + await screen.findByText('ra.validation.required'); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should display a validation error if the value is required and the file is removed', async () => { + const onSubmit = jest.fn(); + + render( + <AdminContext dataProvider={testDataProvider()}> + <SimpleForm + onSubmit={onSubmit} + defaultValues={{ + images: [ + { + src: 'test.png', + title: 'cats', + }, + ], + }} + > + <FileInput + {...defaultPropsMultiple} + validate={required()} + > + <FileField source="src" title="title" /> + </FileInput> + </SimpleForm> + </AdminContext> + ); + + expect(screen.getByTitle('cats')).not.toBeNull(); + fireEvent.click(screen.getAllByLabelText('ra.action.delete')[0]); + fireEvent.click(screen.getByLabelText('ra.action.save')); + + await screen.findByText('ra.validation.required'); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should display a validation error right away when form mode is onChange', async () => { + const onSubmit = jest.fn(); + + render( + <AdminContext dataProvider={testDataProvider()}> + <SimpleForm + onSubmit={onSubmit} + defaultValues={{ + images: [ + { + src: 'test.png', + title: 'cats', + }, + ], + }} + mode="onChange" + > + <FileInput + {...defaultPropsMultiple} + validate={required()} + > + <FileField source="src" title="title" /> + </FileInput> + </SimpleForm> + </AdminContext> + ); + + expect(screen.getByTitle('cats')).not.toBeNull(); + fireEvent.click(screen.getAllByLabelText('ra.action.delete')[0]); + + await screen.findByText('ra.validation.required'); + expect(onSubmit).not.toHaveBeenCalled(); + }); + }); + describe('Image Preview', () => { it('should display file preview using child as preview component', () => { render( diff --git a/packages/ra-ui-materialui/src/input/FileInput.tsx b/packages/ra-ui-materialui/src/input/FileInput.tsx index 16b74d4b932..100c2be0bac 100644 --- a/packages/ra-ui-materialui/src/input/FileInput.tsx +++ b/packages/ra-ui-materialui/src/input/FileInput.tsx @@ -83,7 +83,7 @@ export const FileInput = (props: FileInputProps) => { const { id, - field: { onChange, value }, + field: { onChange, onBlur, value }, fieldState, formState: { isSubmitted }, isRequired, @@ -102,8 +102,10 @@ export const FileInput = (props: FileInputProps) => { if (multiple) { onChange(updatedFiles); + onBlur(); } else { onChange(updatedFiles[0]); + onBlur(); } if (onDropProp) { @@ -124,8 +126,10 @@ export const FileInput = (props: FileInputProps) => { stateFile => !shallowEqual(stateFile, file) ); onChange(filteredFiles as any); + onBlur(); } else { onChange(null); + onBlur(); } if (onRemoveProp) { From 0a9c1c3221789d57584b346c69c07e3279603d58 Mon Sep 17 00:00:00 2001 From: Romain <groomain@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:17:03 +0000 Subject: [PATCH 15/72] Can return undefined in useRecordContext Follow https://github.com/marmelab/react-admin/pull/7498 --- packages/ra-core/src/controller/record/useRecordContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/record/useRecordContext.ts b/packages/ra-core/src/controller/record/useRecordContext.ts index 8d88a6e0a80..fa2834b8407 100644 --- a/packages/ra-core/src/controller/record/useRecordContext.ts +++ b/packages/ra-core/src/controller/record/useRecordContext.ts @@ -37,7 +37,7 @@ export const useRecordContext = < ): RecordType | undefined => { // Can't find a way to specify the RecordType when CreateContext is declared // @ts-ignore - const context = useContext<RecordType>(RecordContext); + const context = useContext<RecordType | undefined>(RecordContext); return (props && props.record) || context; }; From 52adaebe1a4af9efc12f2287d7234dc59a76cd22 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto <francois@marmelab.com> Date: Tue, 21 Nov 2023 10:50:51 +0100 Subject: [PATCH 16/72] Apply suggestions from code review Co-authored-by: adrien guernier <adrien@marmelab.com> --- docs/Features.md | 2 +- docs/InfiniteList.md | 4 ++-- docs/List.md | 2 +- docs/Show.md | 2 +- docs/useCreateController.md | 2 +- docs/useEditController.md | 2 +- docs/useListController.md | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/Features.md b/docs/Features.md index 1d89e8805ac..4c19e532481 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -273,7 +273,7 @@ And for mobile users, react-admin renders a different layout with larger margins ## Headless -React-admin components use Material UI components by default, which lets you scaffold a page in no time. However, the headless logic behind react-admin components is agnostic of the UI library, and is exposed via -Base components and controller hooks. +React-admin components use Material UI components by default, which lets you scaffold a page in no time. However, the headless logic behind react-admin components is agnostic of the UI library, and is exposed via `...Base` components and controller hooks. This means you can use react-admin with any UI library you want - not only Material UI, but also [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. diff --git a/docs/InfiniteList.md b/docs/InfiniteList.md index b367d8c95ff..4ab366fd77e 100644 --- a/docs/InfiniteList.md +++ b/docs/InfiniteList.md @@ -171,7 +171,7 @@ In that case, use the [`resource`](#resource), [`sort`](#sort), and [`filter`](# {% raw %} ```jsx -import { InfiniteList, SimpleList } from 'react-admin'; +import { InfiniteList, InfinitePagination, SimpleList } from 'react-admin'; import { Container, Typography } from '@mui/material'; const Dashboard = () => ( @@ -213,7 +213,7 @@ const Dashboard = () => ( Besides fetching a list of records from the data provider, `<InfiniteList>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, infinite pagination) and its children. If you need a custom list layout, you may prefer the `<InfiniteListBase>` component, which only renders its children in a [`ListContext`](./useListContext.md). ```jsx -import { InfiniteListBase, InfinitePagination } from 'react-admin'; +import { InfiniteListBase, InfinitePagination, WithListContext } from 'react-admin'; import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; const ProductList = () => ( diff --git a/docs/List.md b/docs/List.md index f9fb8f33c48..0ba48ad8d2d 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1177,7 +1177,7 @@ const Dashboard = () => ( Besides fetching a list of records from the data provider, `<List>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, pagination) and its children. If you need a custom list layout, you may prefer [the `<ListBase>` component](./ListBase.md), which only renders its children in a [`ListContext`](./useListContext.md). ```jsx -import { ListBase } from 'react-admin'; +import { ListBase, WithListContext } from 'react-admin'; import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; const ProductList = () => ( diff --git a/docs/Show.md b/docs/Show.md index 930a5d0dd38..2e100a72705 100644 --- a/docs/Show.md +++ b/docs/Show.md @@ -611,7 +611,7 @@ export const PostShow = () => ( In that case, use the [`resource`](#resource) and [`id`](#id) props to set the show parameters regardless of the URL. ```jsx -import { Show, SelectField, SimpleShowLayout, TextField, Title } from "react-admin"; +import { Show, SelectField, SimpleShowLayout, TextField } from "react-admin"; export const BookShow = ({ id }) => ( <Show resource="books" id={id}> diff --git a/docs/useCreateController.md b/docs/useCreateController.md index f1bdbee369a..5ac114b80b7 100644 --- a/docs/useCreateController.md +++ b/docs/useCreateController.md @@ -9,7 +9,7 @@ title: "The useCreateController hook" `useCreateController` reads the resource name from the resource context and browser location, computes the form default values, prepares a form submit handler based on `dataProvider.create()`, computes the default page title, and returns them. Its return value matches the [`CreateContext`](./useCreateContext.md) shape. -`useCreateController` is used internally by [`<Create>`](./Create.md) and [`<CreateBase>`](./CreateBase.md). If your Create view uses react-admin components like `<SimpleForm>`, prefer [`<CreateBase>`](./CreateBase.md) to `useCreateController` as it takes care of creating a `<CreateContext>`. +`useCreateController` is used internally by [`<Create>`](./Create.md) and [`<CreateBase>`](./CreateBase.md). If your Create view uses react-admin components like [`<SimpleForm>`](./SimpleForm.md), prefer [`<CreateBase>`](./CreateBase.md) to `useCreateController` as it takes care of creating a `<CreateContext>`. ## Usage diff --git a/docs/useEditController.md b/docs/useEditController.md index c2e7fc5cd62..85b54cd8a27 100644 --- a/docs/useEditController.md +++ b/docs/useEditController.md @@ -9,7 +9,7 @@ title: "The useEditController hook" `useEditController` reads the resource name and id from the resource context and browser location, fetches the record via `dataProvider.getOne()` to initialize the form, prepares a form submit handler based on `dataProvider.update()`, computes the default page title, and returns them. Its return value matches the [`EditContext`](./useEditContext.md) shape. -`useEditController` is used internally by [`<Edit>`](./Edit.md) and [`<EditBase>`](./EditBase.md). If your Edit view uses react-admin components like `<SimpleForm>`, prefer [`<EditBase>`](./EditBase.md) to `useEditController` as it takes care of creating a `<EditContext>`. +`useEditController` is used internally by [`<Edit>`](./Edit.md) and [`<EditBase>`](./EditBase.md). If your Edit view uses react-admin components like [`<SimpleForm>`](./SimpleForm.md), prefer [`<EditBase>`](./EditBase.md) to `useEditController` as it takes care of creating a `<EditContext>`. ## Usage diff --git a/docs/useListController.md b/docs/useListController.md index d02b21a7475..f3eee4896d6 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -11,7 +11,7 @@ title: "useListController" `useListController` reads the list parameters from the URL, calls `dataProvider.getList()`, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the [`ListContext`](./useListContext.md) shape. -`useListController` is used internally by [`<List>`](./List.md) and [`<ListBase>`](./ListBase.md). If your list view uses react-admin components like `<Datagrid>`, prefer [`<ListBase>`](./ListBase.md) to `useListController` as it takes care of creating a `<ListContext>`. +`useListController` is used internally by [`<List>`](./List.md) and [`<ListBase>`](./ListBase.md). If your list view uses react-admin components like [`<Datagrid>`](./Datagrid.md), prefer [`<ListBase>`](./ListBase.md) to `useListController` as it takes care of creating a `<ListContext>`. ## Usage From 6c7808c95dd797607f96c683f185d832972641be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Tue, 21 Nov 2023 11:02:55 +0100 Subject: [PATCH 17/72] Fix ts warning --- docs/Create.md | 2 +- docs/Edit.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Create.md b/docs/Create.md index 80f2dac1ef6..6a96300b6fa 100644 --- a/docs/Create.md +++ b/docs/Create.md @@ -673,7 +673,7 @@ export const BookCreate = () => { <Title title="Create book" /> <Card> <CardContent> - <SimpleForm onSubmit={save}> + <SimpleForm onSubmit={values => save(values)}> <TextInput source="title" /> <TextInput source="author" /> <SelectInput source="availability" choices={[ diff --git a/docs/Edit.md b/docs/Edit.md index 436365b7b60..d6dfd00a239 100644 --- a/docs/Edit.md +++ b/docs/Edit.md @@ -844,7 +844,7 @@ export const BookEdit = () => { <Title title={`Edit book ${record?.title}`} /> <Card> <CardContent> - <SimpleForm record={record} onSubmit={save}> + <SimpleForm record={record} onSubmit={values => save(values)}> <TextInput source="title" /> <TextInput source="author" /> <SelectInput source="availability" choices={[ From fd7ded85e6a737ad1717486ae23157f37e5eeb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Tue, 21 Nov 2023 11:08:00 +0100 Subject: [PATCH 18/72] Improve headless feature introduction --- docs/Features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Features.md b/docs/Features.md index 4c19e532481..e6a10c22572 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -273,9 +273,9 @@ And for mobile users, react-admin renders a different layout with larger margins ## Headless -React-admin components use Material UI components by default, which lets you scaffold a page in no time. However, the headless logic behind react-admin components is agnostic of the UI library, and is exposed via `...Base` components and controller hooks. +React-admin components use Material UI components by default, which lets you scaffold a page in no time. As material UI supports [theming](#theming), you can easily customize the look and feel of your app. But in some cases, this is not enough, and you need to use another UI library. -This means you can use react-admin with any UI library you want - not only Material UI, but also [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. +You can change the UI library you use with react-admin to use [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. The **headless logic** behind react-admin components is agnostic of the UI library, and is exposed via `...Base` components and controller hooks. For instance, here a List view built with [Ant Design](https://ant.design/): From 83ca2e89cfbcf51a203497d8ba2b52a1ae2af4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Tue, 21 Nov 2023 14:58:16 +0100 Subject: [PATCH 19/72] [Doc] Add link to new demo: Note-taking app --- docs/Demos.md | 80 +++++++++++++++++++++++++---------- docs/img/writers-delight.png | Bin 0 -> 81125 bytes 2 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 docs/img/writers-delight.png diff --git a/docs/Demos.md b/docs/Demos.md index ba3619bd643..920e01d3fdf 100644 --- a/docs/Demos.md +++ b/docs/Demos.md @@ -7,8 +7,6 @@ title: "React-admin demos" If you want to see what react-admin is capable of, or if you want to learn from apps built by seasoned react-admin developers, check out these demos. -## Overview - <style> .demos-list { display: grid; @@ -115,7 +113,7 @@ If you want to see what react-admin is capable of, or if you want to learn from <div class="demos-list"> <div class="card"> - <img src="./img/demo-ecommerce-oss.png" alt="ecommerce-oss"> + <a href="#e-commerce" class="no-decoration"><img src="./img/demo-ecommerce-oss.png" alt="ecommerce-oss"></a> <div class="content-card"> <a href="#e-commerce" class="no-decoration"> <p class="title-card"><b>E-commerce</b></p> @@ -130,7 +128,7 @@ If you want to see what react-admin is capable of, or if you want to learn from </div> </div> <div class="card"> - <img src="./img/demo-ecommerce-ee.png" alt="ecommerce-ee"> + <a href="#e-commerce-enterprise" class="no-decoration"><img src="./img/demo-ecommerce-ee.png" alt="ecommerce-ee"></a> <div class="content-card"> <a href="#e-commerce-enterprise" class="no-decoration"> <p class="title-card"><b>E-commerce Enterprise</b></p> @@ -145,11 +143,11 @@ If you want to see what react-admin is capable of, or if you want to learn from </div> </div> <div class="card"> - <img src="./img/demo-CRM.png" alt="CRM"> + <a href="#crm" class="no-decoration"><img src="./img/demo-CRM.png" alt="CRM"></a> <div class="content-card"> <a href="#crm" class="no-decoration"> <p class="title-card"><b>CRM</b></p> - <p class="description-card">A complete CRM app allowing to manage contacts, companies, deals, notes, tasks, and tags. Built by the core team.</p> + <p class="description-card">A complete CRM app with an address book and a Kanban board for deals. Built by the core team.</p> </a> </div> <div class="card-footer"> @@ -160,11 +158,11 @@ If you want to see what react-admin is capable of, or if you want to learn from </div> </div> <div class="card"> - <img src="./img/demo-help-desk.png" alt="help-desk"> + <a href="#help-desk" class="no-decoration"><img src="./img/demo-help-desk.png" alt="help-desk"></a> <div class="content-card"> <a href="#help-desk" class="no-decoration"> <p class="title-card"><b>Help Desk</b></p> - <p class="description-card">A simple help desk app allowing to manage tickets, users, and tags. Built by the core team.</p> + <p class="description-card">A ticketing app with realtime collaboration and site-wide search. Built by the core team.</p> </a> </div> <div class="card-footer"> @@ -175,26 +173,26 @@ If you want to see what react-admin is capable of, or if you want to learn from </div> </div> <div class="card"> - <img src="./img/navidrome.png" alt="Music Player"> + <a href="#note-taking-app" class="no-decoration"><img src="./img/writers-delight.png" alt="Music Player"></a> <div class="content-card"> - <a href="#music-player" class="no-decoration"> - <p class="title-card"><b>Music Player</b></p> - <p class="description-card">Navidrome is a Spotify clone allowing to manage songs, artists, playlists, and favorites.</p> + <a href="#note-taking-app" class="no-decoration"> + <p class="title-card"><b>Note-taking app</b></p> + <p class="description-card">Writer's Delight lets you write notes, essays, and stories with an AI assistant. Built by the core team.</p> </a> </div> <div class="card-footer"> <div class="links-container"> - <p><b><a href="https://demo.navidrome.org/app/" class="demo link">Demo</a></b></p> - <p><b><a href="https://github.com/navidrome/navidrome/" class="source-code link">Source code</a></b></p> + <p><b><a href="https://marmelab.com/writers-delight/" class="demo link">Demo</a></b></p> + <p><b><a href="https://github.com/marmelab/writers-delight/" class="source-code link">Source code</a></b></p> </div> </div> </div> <div class="card"> - <img src="./img/blog_demo.png" alt="Blog"> + <a href="#blog" class="no-decoration"><img src="./img/blog_demo.png" alt="Blog"></a> <div class="content-card"> <a href="#blog" class="no-decoration"> <p class="title-card"><b>Blog</b></p> - <p class="description-card">A simple application with posts, comments and users that we use for our e2e tests. Not designed to have a good UX, but to use most of the react-admin features. Built by the core team.</p> + <p class="description-card">A simple application with posts, comments and users that we use for our e2e tests. Designed to use most of the react-admin features. Built by the core team.</p> </a> </div> <div class="card-footer"> @@ -204,6 +202,21 @@ If you want to see what react-admin is capable of, or if you want to learn from </div> </div> </div> + <div class="card"> + <a href="#music-player" class="no-decoration"><img src="./img/navidrome.png" alt="Music Player"></a> + <div class="content-card"> + <a href="#music-player" class="no-decoration"> + <p class="title-card"><b>Music Player</b></p> + <p class="description-card">Navidrome is a Spotify clone allowing to manage songs, artists, playlists, and favorites.</p> + </a> + </div> + <div class="card-footer"> + <div class="links-container"> + <p><b><a href="https://demo.navidrome.org/app/" class="demo link">Demo</a></b></p> + <p><b><a href="https://github.com/navidrome/navidrome/" class="source-code link">Source code</a></b></p> + </div> + </div> + </div> </div> ## E-commerce @@ -268,12 +281,12 @@ A complete CRM app allowing to manage contacts, companies, deals, notes, tasks, The source shows how to implement the following features: - [Horizontal navigation](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/Layout.tsx) +- [Trello-like Kanban board for the deals pipeline](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/deals/DealListContent.tsx) - [Custom d3.js / Nivo Chart in the dashboard](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/dashboard/DealsChart.tsx) - [Add or remove tags to a contact](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/contacts/TagsListEdit.tsx) - [Use dataProvider hooks to update notes](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/notes/Note.tsx) - [Custom grid layout for companies](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/companies/GridList.tsx) - [Filter by "my favorites" in the company list](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/deals/OnlyMineInput.tsx) -- [Trello-like board for the deals pipeline](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/deals/DealListContent.tsx) ## Help Desk @@ -300,14 +313,28 @@ The source shows how to implement the following features: * [Mark as read on visit](https://github.com/marmelab/react-admin-helpdesk/blob/6208ab49597544f0e8d7e238c5c676f73f30c114/src/tickets/TicketShow.tsx#L18) * [List with live updates](https://github.com/marmelab/react-admin-helpdesk/blob/6208ab49597544f0e8d7e238c5c676f73f30c114/src/tickets/useGetTicketReadsForRecord.ts) -## Music Player +## Note-taking App -Navidrome is a Spotify clone allowing to manage songs, artists, playlists, and favorites. +Writer's Delight lets you write notes, essays, and stories with an AI assistant. Built by the core team. -![Navidrome](./img/navidrome.png) +[<img src="https://github.com/marmelab/writers-delight/raw/main/public/writers-delight.png" class="no-shadow" alt="Writer's Delight" />](https://marmelab.com/writers-delight/) -* Demo: [https://demo.navidrome.org/app/](https://demo.navidrome.org/app/) -* Source code: [https://github.com/navidrome/navidrome/](https://github.com/navidrome/navidrome/) +* Demo: [https://marmelab.com/writers-delight/](https://marmelab.com/writers-delight/) +* Source code: [https://github.com/marmelab/writers-delight/](https://github.com/marmelab/writers-delight/) + +The source shows how to implement the following features: + +* [Predictive Text Input](https://github.com/marmelab/writers-delight/blob/main/src/compositions/CompositionEdit.tsx#L34) +* [AutoSave](https://github.com/marmelab/writers-delight/blob/main/src/compositions/CompositionEdit.tsx#L30) +* [Infinite List](https://github.com/marmelab/writers-delight/blob/main/src/compositions/CompositionList.tsx#L56) +* [Edit with List sidebar](https://github.com/marmelab/writers-delight/blob/main/src/compositions/CompositionList.tsx#L43) +* [Offline-first data provider](https://github.com/marmelab/writers-delight/blob/main/src/dataProvider.ts#L26) +* [Custom Layout](https://github.com/marmelab/writers-delight/blob/main/src/Layout.tsx) +* [Custom theme](https://github.com/marmelab/writers-delight/blob/main/src/App.tsx#L8-L12) +* [Splash Screen](https://github.com/marmelab/writers-delight/blob/main/src/SplashScreen.tsx) +* [Headless Delete](https://github.com/marmelab/writers-delight/blob/main/src/compositions/MoreActionsButton.tsx#L24) +* [useCreate](https://github.com/marmelab/writers-delight/blob/main/src/compositions/CreateCompositionButton.tsx#L6) +* [useStore](https://github.com/marmelab/writers-delight/blob/main/src/compositions/AISwitch.tsx#L23) ## Blog @@ -318,6 +345,15 @@ A simple application with posts, comments and users that we use for our e2e test * Demo: [https://stackblitz.com/github/marmelab/react-admin/tree/master/examples/simple](https://stackblitz.com/github/marmelab/react-admin/tree/master/examples/simple) * Source code: [https://github.com/marmelab/react-admin/tree/master/examples/simple](https://github.com/marmelab/react-admin/tree/master/examples/simple) +## Music Player + +Navidrome is a Spotify clone allowing to manage songs, artists, playlists, and favorites. + +![Navidrome](./img/navidrome.png) + +* Demo: [https://demo.navidrome.org/app/](https://demo.navidrome.org/app/) +* Source code: [https://github.com/navidrome/navidrome/](https://github.com/navidrome/navidrome/) + ## Broadcom Layer 7 API Hub A framework built on top of react-admin for building developer portals. diff --git a/docs/img/writers-delight.png b/docs/img/writers-delight.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2aafb066c9f69739bd482c58ba803a0926ab6a GIT binary patch literal 81125 zcmc$`bySq$);12qAVUw`F*MR00uJ3R2#9nHN(o3K3|-Q~&?$-uh)5{iIUoi~H%d1W z{%+1W?>X<c*7vUUt>5~^A1)@Id7eA=z4x`ReQgtr3^YlJ7>RIja7eYa)J$-2@Y-;2 zaMxfE@Co4rMGN?Y>uaK^f;-tS7=HDcx0a<Z4h}8%)jwQq6RsT`9Eg$IEek&jeLZ<c zFArgRCoczQ;Xn^>a5WB&!p%T$d&m3Ee(Vm;u5O-+9NW#U9PDmRiX6A4^hNc(Rh?08 zTERZfrojfc9E0yW$~ke|R3cIclm|EPaQ3rj5A<;N^py`(<oL&Z<-zN#k3~4x|8a@m zeMJuCs|T`M=o_)CdiglBO9@L0If_Y2vCGH_i%CmJ%E<|`i;IfMiipaJh)D>ENy|%$ z%8QG0fT#F4xyYNSY5eOM;GH4|%FoYRUPL4yAV4@kLfFg4RYXiqPEJHrTtr-42wWlL z8|3L{A1LJM%lTh-P;>Tm^l|g{bMx|Kzq+HngV#epMGi2+e-7Ql``@?q^!?ZJfQ1qX zwD%Sf6BfOi%0Di2a{Tvo-Vc4;|M74qM-gXtXAfsjKVNXI*uSp@tD~>~@5}$UxApM& z_tn0B>i%Fv|C-zX_G;f-LEg?HCeFTI4}Bb+)&0Q}Isa>5zJ4ao|5=*<gP()T|FJqE zSIhC=mIo~PKRyC$e)aO-n+)Fk+f1B2fo%i;BY<t_-owF_7uQx(z7<%oQJ7$ROY`gT zY&iEyzl3(tkW>C}e_2A08kLme)Ix^YW3irwB;6^gt>+2NJz>fe_|`bMEYt*8dkW)Q zY7cO4Q$L18P96VNOg+5SbjLlK)?;M5Jk)dc&ck05Ri>-wzx)qB{T(&tD`K5$^j$qz z|9tp5My1q<k2=v#V|lvy+Z$VvR^L_Szn8XQ_n#Y0;OLal5M5mWWdyI3@wu`8c*h4J z%}35Qg`I(W=H}*FT3Wh}6bT84+=jdk94LWWUEPpKy~G|S=_e;5vTb{uo|SdW;#LLJ zYBx)nu<oA^M0Ud0?tJii_Vnq+U`MHHTozKGNSqIC4t^S=LV==(T>SKEy*QiwTTgfp ze!0)ajZJ(;@;{CTd|MJpiTdYc(6N*#b))}0NtLjU<N1HaRSLDDxMc)+eKpFMvdV_Q zU5Cd50%&t64H(1K(@URIkTJ`OI`yT+$HV@<1cOpg<`WRem-hIPHtRT2Aa|+z@1JXE z85qQRscCAS*{T!PE%bMEC((^wvP%6i2XFs5)4YX&CjwH}--78wOUueQq@|_LkF~V4 zc5MGWVks4SdwXkZ=J1QRW&gOWww99o5ge`bU_nh;S^1FiKO?haRE;IEeCP7`ui4d- zvEMQ?yW8O1?mpATKg|7HVk&rj{OR)Y@)xcD3^Bq-(6&W^o4fn(b+CYjaQ5E))zM;a zwuX{|(V-z`IUlE6w{B@`YyY>$@Nb0pUm~w21WB>*Ve@%++Lb_6y*l3+=j!Tuv##mC zmRd^1({pR7Kl6C==V~|&*acq3)6>(NO|roU&u=b2`S*GrZA?ml^{lQI{pWg)wGltl z%U$3N@&EVC5IVZL5)u+#GauLgb*GbqFE4Ljh=7Irmqq<AJI8?qGlX6LkI8^n|Jg#A zB)D-T(f{)X^FKXSMlb8hlh-!yT()OE9lnnDvi=nO5X>5!5Ag2F5Pn_z*BK(kmGPQU z@R$du=Y!X5(T#wuTIWGlt7Bj#tPgi{^R64&*gWb{cXO*MXngqF9EC!utH+G)nWcPw z=Q_H!wnjk6b(z=(42i<T#00YXC=GdK_7fuinlKY&VPT=8v-9)k&o}(m(z3GZ{MN^Z zhK7RnS58$+=!C6d>32T`AAYk9FHBDt^Y~E*jN#iY+_k-G`%aBiR?^`o63))f;KSCC z<2Q@7_DTb%NpvCu?J~2WrljbW;KN^ZwxKmX%Q_rhV5+{pz7(}~?GOr-5&pdwFRvyF z!TINDT#Ln-B*txYpZR$DMBujY-6p@4AqE7NhKA<bo4XR+h}zIPZC3{e4w`_?A79?z zzpX?|ORG}CZ*FW!D3mQO)9%%pd;RGfI5~?(3bS0$p3{C!ZLNvc{^rjQh?d?|HqFfQ z^YgT{w7or_z_<D(Nl8iicdiLu^WXU1j$80(t_^)lMzFxKDQKZ9;nBq(XEvqe%TTqA z@;gRX#`M0EO0z^;j)0u?;2D>;Brk8=M91vL#s*TLGVpo{v}wK#jyi>*StVTb4(dHR zV@dBccq3e;!M+6_tV!MfI#eL{*|9hlfsKgRACeeV)6gJeQ&JET^5{usxJ6D%I$h^R zr5_y^*ZJhzTU+wG_1k}g;i&JM??#n~Lq-M$?v0h<Kkg`X`|$AhSAKqeBYOlE#w_O0 zl`V9~u3t<{tX?M;dAu=6)z84d@R(6bh5de%a@4g_jU3^-BSnfyd`$^T=gUrZdM~eB zcU;K`Owztk?+DgRsAyzuy^(&KHM(3gb!)bD_JQf`u#3PoU}`G@R=E5}ZD_prA5=Tl zoSm0X_g5JvGkFbdn*0({rJnp_()q|#Mk%6VZ5^FoQ;ia?HerWmE+{uQH!Up~#qgW^ zR45!X#n2PcbS^2W!%xRE&Hj&{2VM+{v?};4_3e+|G&CyHJ3HD0Hla^L4k5ifYyHdA zGN}B_ps<TDU~4J?-gq6#Ser89b4|s48ykTf&65(~;3)H&ZSe{J)3{osI``xK{Wvm~ z0Zm#w-E6^9O_V`(&llr{6!)sDiRP7h!u)_cG&iSB_;s?o=${e<Rk#by6TIJ&DAkN0 zCMH(&7ruV3tgfzZWyMIsbMxlk?=_AMb_NCp`Z0WHPuU$F?2axOj5y7>V@zUHA+w;M zAR_~k&aE-|@DpL3x2;08+vmz?m1y^$^^dEM#&t!WHbz|jc<G#v9Ml<QgFGGr5rCca zHMV^w)_x1g1VbAb8nTJnMND;Od#%Q&ZT@(F1_Bl<9o=os`de2qaM(x&Dvv<S?=26G zT%2KjGO%9H;%fJN`i0HR%#65AMn3&L_7d~ae&po|lkA!}yT+xxdvsC!>9%2s*EAiU z(70lwz|d}CB9Vy*+u(L_W6*wOSi-(@_R7J6%zag^u!_(p#QfwVg*W0pRcIDuy$x31 zW4TTG`97KFRD(UWs-alY0&vdW_!4Uv&)JqRR+$qvB5o`x!3@;uks2Q~FY233$R;q3 zl(UyDFK@s7akUKYvOJhN)R>CD0@Y{u<wBB(&fREC?(g#=q-qjN5)$mAWkH(JigHN; z<qBC{mtGVB1<D2YBGUU^_0kfB_W>Wloe!Ry)6K`_`G)^EYRGB$+Q*L{`}(vXXQY^? zx?-6b8FN_$a{e3iLBe8U>0Ev%JM(pQlJq7nE-vowf^yduh>ev3cjlhz=CB*)2;0UY zHPzJ%Z-%qx?)bc;i?ph$s)~t;fysH#MeN%_^aqsLhgszPVCWrDd5t$J3_2YS6#04C z{_`F9BOqFFW@f}MQ2O|9&*;w=6fhH&2We<r3oe@}3cnzB2Ejb4Xkxf;Xo=I3w1&Ba ziL$TR&vmiqG1rgZ-`;NjJ#N)*TjSpxKUNGmqNbrSUY-lTTx|H+kji#5MQ^(E@u(de zxeo4A7t}p2O+vp;+@U>i#5pHq-~5e^jUCCAh=Z#MMp`cD?%bC3RB_;#A5!ol6IHzZ z+6uqinMd9e2iTjwxq0?tPcE3Outjx5T8yf=2Uue{`wnOaWPIq?o28z|z{@>Y?4b|? zUR^VlH4KE6)SH(W?_RUXoBDS1OW;s{mJpX^l|*6x9R9YH0LSY7aGn$_ixxi`|G=^i zl`gn~TlS;gLzi9+ZBQsr%Bs*1{`b-^?nG54flJ`_oAUsjfkpB9E9d<vt_guP6aI$A zc>Zi*+0w0#Pf6j?eSj>A81tb~v9!3QW>c#0N=#Zf#l1^gSepY~p@KwU_kOaxP^}$q ziElPD16c5r&*62yOz64-2ALUXVqz+n_BeTosq|UsRJ{ii!&Rp7*&{u}XE09VgZpF$ zZWxcijjcx{dPp)20tzNNy55Wk4LNeOhIk{qKO<^B$jE~qSiVbETx4bh%W)4)b~`L9 z-6@r4T-albgX?F@yDy^XFpI~SdD;T`hdQ*nmjLi}TvJ}DS1-I!3kqtykhm6|H51Mr zYZ!RbeZ<H^IOt|@igSH2d0?e*&BD#~buEM*aK!psRh{n+DaHT-G){fa1R*_sIAu|s zm)GPvS`--xiM#~R4uRddOZmNJ$q6OA+S$>;oP%T<Al_C8J*lQp&-mWx`)tt#Ci^ld z_?DX+e<<|;@%&8F!<WEF3v+Yb-U+Ym86Ptl_E`eBWogOax-@81e$Ac!>+c3AW5;I0 z@pHRRd|v(xJUl!my7gQ=U0ui+?V2y}H9xu(T~hmbzc}&QnDmvU8@3y<V>L9o@z*XR zZo>$AoI)S7$VTBVXBYjwLPR`m895h!1O)th!_57(wBpapjIWO2VG`I_xMd@O@5af4 z3iw)idHM4Cvv0aOZ>$?BSy@NKqQAgVjbp^XnGkGd7j^-JC=*hm2$Qr91>ri)c+KY! z%6Q2o6YQ4Wl^L+N6h3u9S#2gO_8M8TESzH8$%D0iIQ8wxSUNcUW6!7bezb($XlnP< zcB__5EG<P~U*Xz?I6x6tNkrKigy7aL4>l+$=vdG1LZ0M|qDYp)sSQQKOw2`+R3&y> zM<?;q*%aFqpks(ny#Mu;W$PnFgcjx>fch89L)DV`bO5Zd%9kqg^Xa`QEH}L8aG2?M zx_W^d>FO_ZYp8N}Nh@CQra8(D*`WV2KXv{%HC2S4A8pYYFzmI8(C{&&i^QK+6WpVK zOh!(qlnmc~0~;D1j$u}PrK_uZ%Ey%FMxCfZHBm;yqH!n1nBK#F(^|d1t4qzV1hEk* zDNY-c-lN@n7tyEL6w9|xr~jG1LAENz0P$;M^4*U61Uw&^&QB5&yX%YeeMO43W_5o2 zTr-f)YDf7iC5b@x#}C6i5}7qFPH&s>=6U}M0mCGJZOCEwvIx(TdskY2ec(Hp;<sQ5 z+|W5NRSJt`3%T#9`%Xso#}2<OYqRmEqCZ17n2wL~v$Jc5+60;3nYYtj{QY6|Ur7gY zF(HJ?{$gRSJ?hACk>-$W9vgDJb+{jp{of(+UvhxPf`ktgBMqtNuwWqt4!14;Ujfa5 zvS{I}ymM~Yb)=wb+wCbZNun4I%&s@&?kgh&#fIR+7yp%1;Kux)=WqWL9RFt`_y0x~ zfI>9@XXNJxvgc>N?@+Kcgq`_YTU!SQ2MaAT4~%Gxs3sLE1W}rTLq1X4n7068e_hDM zpTpiM&yokr15b8#b^xBfrgaIDsl~;`!GVFH+^j5WL57kQBtR+-4i4a899nU6aow`j zDD{S@m5AJJs`FhPdH(#l{4>pmk;HVu?o$ov30AX-e7{(AN|?xAFh8rPfNnYP&OL+h zk$5$P9D5dOHQzD&^31R}Vmw7t!_4f<D=fD7(VwbppS}J4X2120c7XrLS>z8&9E?^| zmHxoKZauSzwflQ{@t(Kkl|6l=V{ghA6N9_B1f1<(X4=!RO%H!hOiaj+4KM}q8Xo=n z@*6<4m7!Ks6RSEDMafEA!bgQv+ORCS2W5Z6WxvmS3VC8L@GAnTnqzpu+STy>{x`~P zR;v%spZApuZ`4;7B$$a8RF#zAxW%fgYiN}9M7<zkmfcw`4u6C-Gx0$aca7hmXmB3P z*2L=XVoIS+=((@2?=W?A_;3DD=OHF1znN7?di$l<Q&xy1wP#$;qvPp-_pQeFFb=Jc zE{Y@}`WoqY93Gp52b1qyW01VOydpLqEeuqsocqLH*-J=qU?&)F_zP}KEr#kVCeSiO z$|JCW-JGHldiI~4ncer6`pJDn!)Xg#PA*SJ!>^?vGk050SD5l&R6cta0$&3u+bwhR zYj4!?hPbn<DwA={Mej{K|7F=whA1I}NeY~RXzlT{{xxugSJ8Dj$JYFnMUC5Mo^Y6- znpzj|5ji5ZDXnk7_kfoHcYDR}8p$`BFXPn~feY}jxF8W)GliMofBg`|9DQv$dT}Q$ zDJbKwE1aDx;p#c_k>QEBxH$ZKxhDC7yZ%FBoob_Ywc0v9|JPvvJNOEkFOc;5WW*5M zr<9AG7U$q-FHoai1ps-ObG1K*BwGt^FvP%F%?Lm-J4V8Fq|WX0620xc*%rl1yg2r6 zM1>sNc^%9@D=O;hCR&J|bDyhnVI<C<R^Lrd?RazfK`19BP%D#mGtoqemsitMA>Vwc zU9n~hf}`j$v-tW&`*6NYX@<#Nm}IZCOzlI_>GVC7We=MUhO-wIodxsd_D_8>Bs9dl zh_S?w=HwVSDxEz{a)3VHw|^yk=94t^#{`F$x`sO80}xle0Xiy>f#%g)I0sF|&^$@L zsr7bvJjuUm@P*)J8}2FU=V$5k=N_+=RaHtDZrt%LZb2kYmb*6@&I81TrAz%{mrESN zw>gFg@|vveUh!38(IZ<s0RTzwt#M~^l5y(y=HSD7{36gFX}aa9ilxcULnnEa%gGO< z`TMz{KJKPw5Aet#`U1wr#(Ey<stkIopKfWeg?w2OJKIpoqunf}E;KbUnNyUsA7Jk~ zN!>JHU0EJCJ?6%g85j{j^9YM`a(dhEzx@<TTm+ngxDb?4I_kG7rh<z=`Yq(=2d`es z<E`nl`8YO!eSJut#@;mUskZt4q|{s^K8|eYXjJKJ$i<i=x>YS31fP2p)F49_3aIjc z=&R0r$R;3ThVidP9JYaTBV3m3eIOV1=OByU^eP8tP-66mV#S(jJT@jHC+~$N8>py2 zlGqv(dEvOJtcr|`jMqu>osb`0oRaQ-o<|gv4(u-WhF=`sex*~>&>#z~r7Y)gwot2~ z)$vvqnpy|+56A;A1>a;LW#Dj++IR1kG0>y+XT`N-z1l5*ehG*R>&}X_o}2hw-%1Sk zkviM4H{l-)VfF#R`>4lyO?JP~8W!~H%gb+vzrXQ|`fpe$WWuVY#52SND_n;#HQ>ak zC($Ap3@Zr>sOF;x$m>txVsQ>IuDo0)!--#$?|8+@o7fl9Qym=~<r30>XF$+i00g!c zxw8(a%(ClE6ikXuhQ6qib^<ndBmib208QHfUW!h-pCOX<ORfWKX%Wl<B%?Y7P7X5- z5AZZ}?NbsGIno$(N>ux%U%bF0AakE?qS>Ns3EGb(WtzMA{LFCGZe=L9j@Wb?<inX1 z_u&RLtAM9b++bj08pJ8RqpeMt?*F~oeu7cPlldrm%FZN(kUf(}-+`e`B|cPHMq7oP zksX139{rwhkr`X~=5DjN4Sl`oGXuZuwfQMtER;6SjvbYRVOOcX9&1(WL^yz@(@1N$ z=%%xs(;`vqnWwD6&3*m}IP_n_{)jJx0oLk*bqPjMBLx+yB+Nc($@-`o0Odv9V#$;2 z8QuWQKJB+;ppNgbj?Y=^HLG+hPa(+1$%)H>0%GRz?F~y9;u-y{#+{{-qP}+Pg_p?` zEd!{sBi`cBNJV`C+3x|fVR1W4OG|HCFZ1TxsH&On`JV3$iVR|xR#zQe{alhs7P)SV z+W;#oeX~fp2E6*hsA?RUGxgey0nz-Gp~C#EEG{#+x=1WJn{aLxS($Vwq~=z85GIO> zN3U;qnEMAgqh$R4+Q&y1!K!8q<>{Q73#^zy9;~43Y|tvpIqqV~CP?hZ*zQdF<xaNW z^Z4?@OgCG&TWlv(Y23Ca{pAq9pavbrfquBI?_=9%lAAq2@d!TcH>G;Oq%lum_V<!j z&rcRp3)*$Zq7S;bmf5+bp5P^SqVCPOMLf?JV)kK&=H<3AMCt-B8$agQliZ%)m>3ls z9X+?K$Es?<uN|aXBG+ll!M-(e%YF}8ipd2;SnqT~!kQY4Gi&Z`Yz|!r#Tut(3Op7g zx3<64yxrlq&h&11SY}pD4DtqnZBS~ACe(n}+;E0T4g_q^u%H3MHZSrc<6(muHG2mo z3+!zdF&CVI_1;T%oEkhw+U8Tdfx(irYC>-xnkWfjObj!7in3@KK<FUnAflRc!jq>U zozcTkp`q+kQA5fO>~9@CD3a)Qb2gl<ssN|R1nF}fRoExlavDL}YPBU_o^7|bPA_M9 z6QiKQ%EHmv6c(8JbD9+**GM?#R|7L)2A+9@(4G2dc-oEJJ+<o`i!a}{=#+tB@(Xd? zJHUu}b1MAZ{4jeNphNVYy<O!FJ{DU>OlN1TC2AXkm&ZQRx9dBqNI+QfwfOJ3s|#)x zOLvKmC^-i&g5pmHgHr8W1@TP4G{{l$qOukNg-@FonGr1o;b{sx-|f{iyGLOlM(j&! zqcRW&ZL3?mkz*&i!^_DaPy9N1jsxvIfa`?cUE<KCO$5JQHKKRk$##j8kC5Fy{nqLp zk~iE{r`5Vd|Ad|4`{NXNpj^q_d+jcmF{mq%Q8KLcQD-axX<8$m{#(lr-Vr)#ZjG(F z$HUTi2rLP1=VpU0C#*PK!mbVHT9PnTBy58jsaQ2=*Iq$*!=h+EtE;{)hDSP;H>*8W zj-5~JPu++EUKEx?evSslEOn3ieDC}2qHM;Lx5s!z0;0>QxZdOEINYAdwVZB8uJ`WF z94iTP=(ibhH1jLoS;BZ`h0mS0$3BM)qQ9ft&QinA@f=MG3*zfZhKnx`c=<a!q9kCw zI(oRKImxlSePYnynEOZ-dAwfgHY1-!ybO;Q-Ztj4MsYGTfl<3<bX;M~Mb`X3#AV!) zf{2t|*VUI~{IY4a-%~qQyMst@f~T=~0(L!Tb6eo{>kePN+z?yq-Y!vE!3PvctFu3U znwow2804GnYC(R64U_Ezj{a#k218HcIm$N~&3RVB7e&Tr_?pfp5Ens`89E!2BR-aF z&Wz=!HE1Bc>rYKe^6->X1%dU^E3u!~N=EX%%aRz9Z?qjeM_|9xcmSerFQ-*aO$|8$ zn-wG?Z1xuKV2Gwi2%7NYiNx735wb!VCGUStJ`^M9qTo+Jy2MlixHSYf;grk3sQ7b{ z<^76w84x{1T#Q$>UTk`yRJc^Q2UHI&yg6ToogHy;LCU6tNrSNgmQAnjd`RiC-I#gk z(r&u(NY0KKXFst@Or&1}CdsLl)wmG3?<6}=U5(bDZnnj*W^8v|a=F>bOJ=%}!jk*5 zSV;08rzJ(?(}9Vn=Nq~G)79cdpX5iszrMB%eh|yAXJ>Do5m0z$TTdC(_x1#_3h35* zFPI=<fI=o$IzhrDDVJKrwX+Cko*S2oAK$+(ei2PbwPT`__-+h}9?|0bL&3(u$;of` znXwp}C(aBHA5#F#vMyXrfvu$j_eF;Ut&r8y1xt;731CTehn8^OC+K#0Hh?!jtH{(f zv|*A{#zd<4*kWA@;h|5yD=vVDB7A@{!1BCmm$-gifN(_5WT4Y3{%KeLZJQu<luYoC zBgw);cI;wrYQR)}Xtkj{ggjFjw`&uNH8Q^DM$TBm2uf+AAPHO-_dz8oI4s#kck5=$ zZhfY=dVjB5M-L|1PGF-RosgKwv2sJWJVx8mzVWmBpD#6znih;aWeCA!r4-#(ddLF> zcjB<PTZ9w@$@mm{DW#Y+_2_CrG+9UeI)3nY0QC_Mwhz7g(}x5t-D0JPu1ubaa@7OD z*Odx2UMwqWPMHe<C`yRTyMtRdyM*aNh16hRpclFIV*zpVd<U<>AE2PUL&(O|gH=I^ zwVuaJa2z#l8r2fSq_`CPcP7$N#?tZu<!l?Ol4J-s$8M6a*_qMdsU)4Gei?dB+~WE0 z3ym$|%v$>|zAGc|pB%Geso~|HK<GNT*6H*y%g==hMdeLR3diNbHt-W@AyjCZ>*({k zHzK7e#x@dXpm|oz%nE@!(*d*HXH`<p1JqMFRora3=pjkYb{^~ygY_&MWSAt5wzrvH z#<Rd(jY9M$uYR7IFvZI4&|)N&C;KiSB=_%n7hzN+xuhv>oPYT8ne03&jjJ~nNu^nh zDLCN4b}>GWnEa~e2{_|b%#@zEKs~Ikk4uh*nmS(ch3U0yN3WG#LO{+!y?IKLQ_e`m zE%%N5rOORla%O!9(Jmt$uVXgb&oS%KoTbT2KAfa~#2va|y~D$=&SQ|Odv+nzvB*qY z?WeNzWsm->^RBe?eiBnHvHxX+9QiD1+*c_DDgt1apwDl3o&zK>4V*yX8I&<n>$w<< z-;@VaJ-$1{!>1LFswDTHd%Jk~gYq;@Vx8SCdl)0une86&fSssyx=EdYa88f=&(B6* z*Pv+TUJH5&N|L9cK0dV`hUKLM=xjEMA5g0;9Y|mHlzx{dsD1QaDiQSYqBY{-Yf52% zzC-hb4GUzAr|=3r<lt+K!oD2Sb{t=CsgxLF_CqcYMPLF9T@M13;py>m^piH9k{Kji z*0800)*n3Y{;c=lUpc?jj8-X0PjIS=WWsb8ozFx?^pr(bXq3BHJC&CZpjYS}?hV>| z@R-g0S$O%q0t8@k4s2kR^&SrvBqem+7B2;@8?Y1L$dskRQIQH+bS|NzL4uu-(Ad)U z%^Gb0-L&3vFht326+Lu;a}uLt2X%lMUNIw{NSht)5(dp{D*ntMJ7FM*ZLcP?@AK|9 zym>)wT2k#k)K*AuTl;P!A|q^RoC6xwakOZy0IRc1gP~n8eWoeDpOSpiFBz^glSD$( zkb|i(gtkga?q~<f{#GnK;YY%{9ns5-M9h5m*6~>yZ-H4$HcgQe7g(|>Rja)?WcknE zZ(D_rP5X&la(rvv$8-XJlt-19y96~~A-}HtTbrbocsTry;upIt!m5M2@oy1W*|f%_ zZT%9%A_e}|y{tkkiA;8{UoxHal7d_YJ9o%5tR-Hh1?!ovEwDiH1xe_KhaOTpkG3Rv z3J@(`zlrQiS(0^H=^(G(?zOQyoSYBjOa!;aK6u#@nNqx!7{?4k;RY&_v%$ZGFlp}d zT6%gU#KfwRi=IOfq;RtmqXxdsmW!jGdQb)CM7I`Gp3ld>z9juZ9j@ei{kU@@`>h48 zai3Bs{VB+C1j8L29YKA4U?U=@%_aVpR{7n?(Zh_4t?6beEp;<KVr`bN<T@%|gF4UY zJOkxYqdA)=zrT%b0_4KcYgnwr^5~j<B!vURkL6Hf<Mdn6dGC<jMy%v1c90HFmYj{+ z>+oQrbvA`S(I!Nq_G7YYBKy_v3?bvD#^WHFr=h{53wE575)z`h%fkU!*2XpM99z!| zT?PeGWmObJGl%WN2{St*K%+GH)e=X_jCcbaiBkwwyuY2vegq*Zs6HEzZK?^@{FwQL zhmK7ZzX#=$*Y(~0bj*4y-#RJ`c9!Ni5#?q~DV_R6wERL>jXIO*7uFB)J7sX20kTGy zSbCG0qJJkOy-m}RVI%fNX<LDuuU(t0$p_DAQ9*9PfDhB3>P3wayD80g2=N;d@zu!e z%|oA5aJCFYj9X5UiirBR=>+n(=-fJ=iy{{lRPAK8M`@kT&(AlII<KXF!m|g5=an`+ zV{Cqh9-2wUtK-L*^%kv!zJ+`IE-9agE~8%$X3w4KhjS<c*(5gAwN~ZI&-tYpq|4M0 z7M9U#<1Wg0PSJm$$fb`S5hqTZX6tOr&s_|po>U{s%E^()I$*ioyUO1)3+R@lRSEAQ z%MFe22iKYeg%)Pk9A_0Oxo9lc?Fme;E!&3fsyAud?pRdf*yxaz(VE8?Yqd>!Za%mz z$v44(uI%`GKKi%1HX29lxAy@khc5U-Z7HJ^L|Ev$#t5;}qz*4cnf~skhF>vYskiLI zh8Ao*xu|Fn%@Yn}i<5eRdmL;DVWHH7uXMD@jl4%XYuG3CXim03&eo{2#*0It>>bQQ zB9_$Hd%&s`DodT_-H%I!GyGTwYEAnaYV^)Eg27;<L|EEkY_?RD;OpCWF2fuKhugEo z`+n%uM?Zd5T|-2jjIi(JpgMNaS7FQa`u=Tn%`ejdDKlVd1Y0119`Ta&UpSsVg;72_ zU8dvUnNe9l-=V7Cp@KmXUhjz;dq=U0qH3;`)t`2!0re}xhx9uS!cYtLwj{vD4j!4% zJ8xVJh|U?8YfPVS%ABq0VClm1G7H}-iF`RAbcU1d(-U;$HLiOeR8<P!&At(%B;`~a zDB#A*p^Tf_n`-^{;?q5*x`k<x{ffvql`X}e`5(i`(qyn-jqYs6-JN>4ofERb!ux{C z(84FMk5}&jD?!P(YHiEscA?vvYMJ7XMJ`rqSMMQlQ{!r6-*^z_ADucJ=aZHnvAnfc zPf^TbZTWRRLrosDS-cczbu7GJ;@S$UP7Y|@h-(W}5a@jNE(qshcvLYwU}l_lF;nE@ zAhtm9>zYe`QjKs>^)chE)7p=u8k_y$=9LQ%7HV1&;79u~!^kLOhBww5l%A8pXY76Z zG8+;rt|_%=U(3N^9#fUMMsG(Q&yoZ^aR6=@5!1bO4ew58EM9n#!0j-5U&_p2x}8(@ zk8$-tIb$3GL%;D7x+I6^fea_`9j*FfcIc0@b0DyY-}L$DIo)(`=1Uo%o*`jR0)|E1 z#<|{++ha?VSNcCE@<IVK{$3MQ#sM4F^7k_2)U2387HBbMngg~$w9I#Ppto-Fa|8Ms z0rOYiKL+iCQps<1#Tb=cKV?F{;k4r7n?Pm-SPUVcwm~3DH9pwUq~iW}y8VBVQ2pPK zEKMVg%*>vEA|?yV1vTk0P*ngxdFzA7<tOj?jtp*Hz{=dW5|fb$nE4n)Ct?$7giF~r z{!~}7NXA+*BO}B7t<9!ke_|q$&X2cK<($N{f;)jVN#I~Npw0I@8+%h<y%NlNl~YMW z)SY>a8!Ocbh!xSte@7paF$1!33I9<lj;v;A=+&-75m~o{=1%c=sRT6v<pA0aRF?sa z=5!%t5U*(QYXG{H1pKqRLP`h`eo3$WH>`2wY)7kZU40wh-p&GUh1JRjpx&e3T-bU* z<s2nZDz+(<MT{PsE5Ot$03H4Ok2v%VKrBAhfGIdT9P_IH)r-rI6CQvdUHO!n$^h*F zCD88=*ale`7}ozb>sAi|Iv%eoPo*?)>bV4j^c|Nt<?hQ;=R;yiu7|lfFBBNB>RgPB z!RIHtP2<1Pfh6q!C>Cy7-KE6=RJS6~m+0sWTwa`oWlyeju(<)6h$BY<VR&E)nMT4X zDKpa2aAKZ(Grf%$ZliL9VxM{lcvflYZ0rZ8{*1>=GD>v=A7&r@=0yVv|0vL`cCqF4 zaI+Jj(=QO%!5onflcLnsW-rY_jSW&sfPMp3`snbhJf2UFDxqYRx~GDpa6U3>^yc3w zHi#l9eYa7p-Sh0n>lnQG_tdcW?ju2q>b7tQ3l){=<0`N{pW{I>51a<h$YR+Ck@U(I zb+Z!}T*w*gKv47!KEVbk--Llm@slXvkH2xEHNs&61WVsEd9WCVsfGub7=z`dCF<t4 zsxl&`Ss&E5rW!vRLiF#(H-ud?mfS@(eW3)260GHPC4X%V6_g_tE@ImvxPA8In6T45 z9tETe`1ttX3`7X}w1C3?06zXn;I}Z|cLg78v0?iw`uV<##=xp}s_=4@&UaMHaK+Q@ zY}(p)g?@97ewd~s9@LG9L59?=HEf@xeM99}jtzgTVsi8I206gZi-XJyag`wTMo?6a z^gNqhWFEZn__1XL{cGQTIBJ9mPyTrJ_2YGS$Yb%{-Ce-SmeDw)1{W)v;ix65$`@0y zT^-C=<zQX{4glOqXv09A@fPrm$GpU#o?I`Ua}xO0_;`_puIwF2lLe;4X}~vO{vCs8 za4kpP&G43>!4JK%^0a8_qa~4LIZ=->$!>+2TF(t{FN^7>`aPnU1vfRIW{~q;adL4P zc;N27vCy4_5EE6v9JGX;MF-c`-VEHa1LBkPJPam19rt$`X5#ww>t#Wg1FVEZSOsx@ zjn^-QPcV6T*O#6&fjZCn`g(y_0Y-cYPk1tfJS{?L&HK-RTON}1o-v6VpQ@JPTG{uf zPneCSL*lM2gUT8IHcK>;6W6%bAVQFXKnhiu^)6uB=EuOuNVby6&;f-*URG9@L-~mk zOWen@Ynvuvzyke>4N7rtuRp3eTk&46(n{mfsf|7(LRTV2u>7WC0s_SPC`PmiMa|h= z%m;fWh`uHk_u5CG@47~22JlA{C|kG#GOJfhqom59oIbG?u+B(o{}P<X<8T&jJw3Io zSCz6}%!|7KK&s+cSAa^@$<24<$N4t`gvsO_dq_fQ2<S!aaJU{$H$BvQ3=|LTBF=w~ z*T(Q}$H@X^7bOQwsPc+&O#GZeFBaKIxSkLnZ%2e2-VL`H14X9Ag@r3VJIG6l1npu% z_1;ds`X?b3PYKC91f69s&rV7RaE_4xO!7w_t$qONN=!0D$7t2$&cGWEBCyJ1EsJV9 zWhbX4khI5(#_q>>%?#B!+XQTUPsbnw_%w15IeX^!lB-=uS^4;H6K41o%Z2<t@@OiU zTX_9p#vy~!2OP8{o$tRQlYooa)-~WE`*=JP5(~B=l8KQ~C&)eC6Z+xFOLGcnvNGxe zkY*kv>3+vGVWy;1k!%wm%obWCIVwFHoFZd{4CDG&a;ACZBe#<i<~i?{EXGdV5JqdL z=Gn<1KI|v7VIc27dXz5OohG)H(mdJ>pslNQGTY6f>+ufi%8_;vefoZJ%G5RVY2rWy z+I~kARH`!9`!wi|;)&5bnxY5YTg`41>VLSh$4wQ`=<)>+rG|b%S{Wr=NM1O|kkq1f z9qt5yh?P|aq*QNhKK1GO=uvZOwLk4Msn%9`GR4PTlD3(q>&JNq{seX}n$`DZ21G?0 zkfl2HoShWd(pccjG~e;jb%~UTQWPA-S~$oreRs#_2~2+(e^Oym&ZK8R&3@d*Sf!hi zD|Mfi00cu0A?>6!4R~roLa71;c2VPfm#rL6^*DawgKNta_^#ah#)U7rv=&7A$6eX9 zS^4^Vfm+X=V~^qdNg~#C+}px}o{Oo5o^<-E@+Yiz73PLE?X!3{Xqy8{`WrgJ_!BIK zQW$|dVh=6HNr;Zt(H6XRyGLiqn8E=j>0<r<9*wI}hK)D!4%f!QSwKk*<rguQNl7Io zn-`gWtsu-(a}t1sU>r`@@SRW>xR#{ct^_#WXQHR0Y;-RKt}0|{H?T(I_MNf)7q`!8 zFM;6p%R5&?Sq@XoWV>HPmG>bK-MqGXFaPitb?;Ds%j3lv^4J?bZE8`3^ZlR%*y6v* zsIU!0a$H0Yw`UZC{yzH7WZHIzpx`x7WTuH`QLJdHbD0(e;Q}Tdkc#4;6C6=0X@{t> z_(-LCpby<kdkxAvz|j`$`H|Lt1L@ZSWecgJ#69+3F?2R*XRkn6m!CGGJYao1CN-;+ zgZMW6yTvVSJAYrC{V48d6_xe(zmY|1$>dAAbyye4KfI2c34wBuiL#Xbd@#w2zahlz z@|Tbz!>9*0J&}yDQfnme;_%yY&#zv#@MN;6Yor{ncGKgdpH#d_jz?g#Dro-ffeOoa zXz=Y&1x^bq^REpL93drgsrD2C;=nWXwKI8rR6d`_>Cd2WdL~|gv7t)xej?Ix5-Cn1 zMynC|%7o9|i&I&qhUQ+{1akmqAP|=%ZF^aZK^YZ}r+ncK5b^tRg2)Z$>K(-&3)gFM zu=lwB0!F{qeaGCU)JWke5a#IHNs(*=(bKE?euf=66;W+iqBcBETnG0%leHWNN8~k^ z(IO!x%ET}V6bBibHi;TXZPvzDpg-mRNE)n(t3<g)s?Mp@!D3tpwe#E8E&(*fC_`6I zPmhmi@Bs=U&>xk$gaJMta~RRag9SyPxcg;@@#px(u~Gv<1aC69PIQ%aCN(j_L{zhG zkrjQo8E$lhb-Dx5p_zCUqIYQtZ;2g0042JWMZ5i#;k1V!PsiRELAF$B(eMdQZ-0J9 z-kbbpa&P`7nx|Fe^Zc<SVw1KRn%6Gm@0C{m8iDP5M)#^@O<X_i`da0bPPn@ffFSHp zMk^BitWQ*enyqJBH~0j(uBAS4P$HBpasvftRWkOV6VZK&>-CfP#>8wq^>yx(d+)#5 zIGP0hw%Y<+gQr`bC{d6zq$4BzWIhfAZ2%mp@0`UdT1l=FdUh!`HlG8fpLlu<(l@qI z`y)r2%EV9)`NF8c+m9T*2aFK?EI`BHWo9|9-Jpn&ig^7Rj^cR;q62fzZ&i!jL98r< zoyI=%yxST|+snPM-TJo$016s0cH&0^ZD&t$G7B#U#D3H+Nkj8m!I+tI9#9a?RGIST zBdLDVSM~MvwVp4FNIH%9QZ9NUjegCx-ed_{$#or=eh=kW?J<V=fjS#O7t+dJ_DYgE z4ws}8AI2G@(-`m^gQFZovU2d6{!Y3U=RQo5L9dXES4*=(q<Zf>7syCB(x}xh@mT&; zr~`~g=*oCDvhTvXw^B#voMOue(LC+Q#R{DEcr8*wVgG=GypT^&(=|@qq;p;<V@*Mh zKEB`@qN-bXl!r7;JZ5zgnir_g9`!1dJlRtTC`OeJs7-{SRxL1L|Mj<;1RfBfkdheW z=XMa}`dRP)SQfFj`m_8l6_A5?*GQ0tbqhFDofB@ix$L;ba;Rf383|p^qV-Fj0)=Dd zk(%!x?w-aa$l##JxmxE!9vr0+y_{C(Iixbq&E}0PD=R}_1qp$+o1UyGH#4)-_QgAe zv>66d%ofz4xQ=q|8sr^8a92FVj_!!4rJajeiYks+>cgm2!n;w@VFzQH4II{_U+=@} zL{&=P4GjP#D@?mvUCu6BPWVm?{ey^?KhH`@(b=hbK@G+L%#Y!I;H4H_WUul$&Z?wX z8!MwwXa6<bEEE0e<HvL0cS>wkO2)Do243;5^?SPD-aDTs4{!KA^LumV(~XK;!k<9T z-FTL?_bL}4I#Yg<)POB2R9R@V|K<y8pVN^W+oCxMc3sT;{N&wKTUxDo(jNDyP#g(- zhOb;ahLlIrIpd}BV+m2=XFq3`Kr-<)Sw%~$4@gk3zW@pL2+OhtJS8R7bpZjf%F=hQ zn6a7kKEJ;T1^-emS=Nk>jgod7S9^3W13-P?^)Odvx2%)Tz)i)P`43{DPB~N3PJQsw zci(e`nfhaqy*)h(>M6~MZ7*D=7xYWy>>4==mm6-swecZfyx44BkwzaG6*>5vSpx&Q zdu2cv&MDKl2E56bijGy73FX@a7B2GVPd56atop+&PiPa`ja0w>j;6n)G=C057m3<K zoKi~se@*arWbUY7B#n~~6(@52LH7et-3e2s4gD%k+#{_lP*Y-nya-ssB5p<UfHE;H zf{TVIGZ2=3<$*f@4@M`fO9$!%1aD)+K0k*cT0RrYYh91=FvSS7Kc;mHJKbA8;O6AS z%XLzoye<@0@y(wtY5K;R3bS^N53+epwfXM@Wz`PngtH~QnvD6DIxTzZq~v5ST;jhy zYCwVB8AqOjkMnhft4ixc2DeZxCe)FNt6aehcClnI)YpNxKd`tthwI>B{3M<GOKI%J zjIcl90dT?m+7Bsjenm27+n?C|1mtPP>)g=jaP~=2uMA<U2iFeQ%1=$o$bfzaf%6A8 z$p~6a0A%;{6lM+vpz4)Y?%)tCUff3LCsPBFtHU+rXBd&8bW(f!;_yqkE;56!FU$lS z@C6fLJ3vCip<i!<eDECNv2gvI+%Q`aTWxPY12M;hpAOohlJ?3q9{*X&G-Ow;8}U=+ zK4A;iaGz8;x3BMMW};NEk{n7nRY|oVglEOOr<}QyLMe4~P2rY#Ti<KSC#6aGG*V#! za3ivK<MXFNJg?FkpAKe=Eu!-H)L>!1Uw?37B6P+~>)q>n>MrqnqKYcasfCINMWI_O zxXS5He*;7N&Fg#H-ki4qSzVE6j6Te$i`g_{N8OJjA`QO>fX89}ec+vHtV+0&;6GGk zk3<ASdPf9#=SrZyd{V?XfjdB#FdUl(@W2@J;^Cj)ZwEa>dM|izI~^2NsAAS{n6k2< z$k^=0I-c7T#S1EOOn?y3ti_{KE=oXcw~U1T9xLxH>rP_O&;OWFR5G|rNQg>#DauSD zOlC&}e>oevuivE2>^bqA0x|I*M}rIBvdKHyi58(HO<XiSZbB;o>uUe_<XJc9!ulF) z0EG6vkDEftBo+Ey5|xNCAvMhOCMY`5Uao5#bk0NjW3oGM7KV$@71pUx-10{F`0RN{ zPYJ#R8}&YxOwFv!mJ6DUF`S>dd-Uu^)^6HgY;WrK*6xF^?MnOsuN8vz8MLG)b26*p z-=)nIw!}U|d548gt&~i#S6mo!>Qe~ai$5i<jF{PC<2D7-a>lG?Q8usXCg-C2xF23t zHk}3)_kJE$fc}9(8Hva~?*+cWm%y(bEKqT|ckjB>ggKJc!#+pz&e^fQvkktU7&-Um z&!3XYxW$T6<T+{LtfP6zmFXAPsjF>UeVXeg%=>u<tOJYR9-PTN$f|3Oa5!4jQIlzS zzfR4jSX8~+N+V>(5K#PeANk~`b2CDr=P$22Hu`UBrzsz`Az9HBT~%M<be#AK4*Da0 z$Rm{^kMj!UN1I#9DL`n&$;r8#00muLhf>~v0y5;x$jn4wXIA(3_rotwU4xXYbaaS% zQ*C9gdUpaB`?;||)ed^F_Fl?%Q^f#H1!yLCSyIxrcs^FHxr=xnaXPlN7)OcfHjM*x z)RmYYbebH!vBaH_Q&M^a4pa3iXboces&D>}W+2WYYtZ1P0Qrx$6ukQXvK{Jw&|i}C zVy(Z1|JimsC5SWsR|_6T2@r?AF#_Vytn6%BYU+vOI+K5P0S;Y#;YCW5$T$bMHXvpM z+}=ML^o*bq|NqX1i3e;!AqKOyilsUnA0Ia;kljUjuqX!mjTR{eS{-%+l_d~&8gGnQ z1vxyB<>i%@J_1$EhumphQQG%KR#%4e`TX~mw6}GWrE-dnYP7)CYiT`y`_(ydd1d9R z?r+dk2?(;6Pt$5^#a*_5PUeNkRgDyAb5tiM0R!{i-I!wPUkz}c81^qQv9ZE;>W7Lu zUYfrKO8=jLjJjiD;seOayn1}~-`LVlAlC7&2ij4E=kz&N_3lA{zxvBfM?>=kAw~)Z zkrb3)@-j0`dB-aa3iApJhi5Suj8$R>0S_o21#DTTTq!7RY&?Za_W>w0F*2IFOZ?*k z``+jxGDS<zQTu51=HJGai8OxcN9QNOZY+>#(9vK;R7(5`sqvWc5Swuy$d6>b=cu;m z6N7;vUo~j@skQ;y<VqU}*ga4l+yL!5m*=NI`j{)@Ra6WO2Gnd^?K{9eF*7j%(du|y zwuq3>`<oz(c>y$3a1@N;@qk&g9uU)qr~d^ay07t9vM)QZ2dupJgO~*20b@YwXMP*{ z2MBchrt<#?vfdN9>d}Iu=vT|sllJ!ZHgo~?&}0Zvy#H13R9HphwL!#lK#`u2u(4e) zX5QFWXi@Z>k|hIeSHG1@K*QUexO+FL149m>dCO}R+L^S%)_t|n#_{p-fa7QbH1P$+ z)@NhOQ&>_r4DcNxlJ7m>@<1oSB;$Dtw8!!lnL|KTi2}3;&06sl$%6C}rlq43DbvQ9 zC*$>Kd4PuA14t9#D1#r?v<nN$;r*XkZuo=JG&{HU==sAb1tB){;$8+2j`lwV>OrtF z)+5BA=)UZxGBEY}MWpJ+RAXk7G`iAwDKPE2TUKVqGk#9WrR(DBoJr7%Q%MN^2Gr30 ztVp1jx^HMeMU28Ao-Tz$arwI{8$ai#%USWQ=-NBVJl!7&CCfIx8m|Nt`ZT+7ITDrk z1F}>^)ly#!mu=H{Ct{5sYQ^}vR<K_afvvQq*KJ~BU?3Dz48K(Hm)`SmeeQ!;<bD{& znE6$modD1?peIxVcc7x8BF*&a{RI-%uyz>eWt6_h`OD^5$eCFLGJFb1SZ*GkL<_NW z#Y)t|=6TPEiOEXR!C0NmI_)04->MQ?+ANd|iZAW{oxS$}G#?+;GkfV=_kC=+4<Mnq zvuvkkIn(|Dwp5jw9wJok*5v5Kc%e__p!ykpTdWpjBT1~Cpd&d)&(L{JLFvc7dMr}r zkt8#yIrVrcv`D=q#n5nbbH8%v7SXkikMhZIq{OqtzUd&*)73Sd^_T-W7@WNhbOko* ztNRzr`SK{+?JjiJ-p!b31{@i1y?1waW1Z3{?$n`b?tOXD(V;S4v7@W>#_E$iV2Ssl z=?t#=IeHV^0RGX>msWqJJrs*%ll8Xi&)|k7r>5qJ-+p5Qx|r55VvZ?nH$giX#0`dq z6BA)<1cAc(@Yk2<X>fweu6N6-E-jA(q8hZ$M4I#+x1b3mzJpT)Fe98Vz|0<f%d*BH zW=b!{KRPC6Yz-8rv(nSiPTvXjh;zF}L5pL5Ca)t$bUurq1zs*8AtC6lDAnXOETVSY z0%S4hhx(4jGEz{W6OOi~e^zKy<s;t(mj>MM-_V+AEv~TujTkg^f)+UI%GCOfKY>v6 zK$%RQGum8>0sJ=TXwtO?ITk}q-8rF@WPjE6InxX}A<aHs;8J<Oh&v%b*cprbY+HcN zB$3Zn7Rw7f{Bpa7COK35s-44mK<h`@VFDGe3WjWUn>sWYXh$)`1Kr-7xm%7q(Oq2t zrIzz@wgmQ5QZFuoDveXX9#G-C$tx;;0yzSZPxq**G3p-xYVlDrAEnTd{oO8{$>YAN z%MYy=kK4;|{5vNKl(o%`>3xChI)x4`EG&Ep<Z?jD5p8jxCQ#rCy0L1B3(iSXdr_cb zD)EZ(kpAnM$4?NuT}N}Hq_E6h&q#cUj&=3*X|}EE0>zedt>^P)$_ZVa5=q&VtSjli z?&NxeQKEFlZLd)uZrq({Aoz-uJ$mNmLeRaVSAZU|`RJD*I4ahO)^GCP9FQ#441DkH z{3<#jO&j+W8eZohJzpSbhZ$^mIuuqhuX~U;@&oz@&`YrP{@fe4UpFRDT?yTXI42w# z<%QdkR8~3KYT4~`2k|dqa_KoTC@lNHAbjfbqDh_N6-ZTvle+^5nSsymdV23IE#&6u z64k<ucYi=2idI;8y+P&_3s1lputM`BTF>{A^wxV%Uz@Aq-RCN+Mo`s$6dUxs9JmYh zjq7<3*ZFGuZ8I^#l07$5%h7vy8ZtsvIy7F~YQ3C%^Cjn1OJOW>P_y<18uBfYAGF#P zN&w}bv_0-^u3-3xUN>mqFLt_5Hun`^QBnf$W#Dod%kVO_el*iHsR}_DUDJ>P{>`yS zU*tow4e@H%No~O0-SPqP07XopOvOpSiUpM0Nal8ei(V#M__?uX85r@P-%>{mu~KV& zY{#SBZn~8GiKlHqF@j-9O91C%P{_YeG4a#hfPi(3L_3}=yGq9PJS`~IR2s|c5~%Yq zQv|n9M9Gx!4sfa+WtMO;XDNZI-x;j+^rsh4h-DzKdP`T*4jW=kv~roWUY5hHQV#sN z+3(r*kyXh|G6rRmf^oYGzknE$m#WwS$e5x=9nOK^!HXrwnT$ca>nA%~=nh(ti=J!} z?=kl}S!qP#PQGrANaw^;5}^f>Xu9vuEDNyAo%VxH9#M?R_-?ScQrk3_YzBJLGsoxV zudQmR+X+O8vo-Jd&}vh&Y6LY@3B9&$eI(07pyhbD(2(E`ieZ%zh+Yev!Us%Ha9;B@ z9tQ+@&9}|Z$1u13>UzJ!z)%U)70H(!u?#E)?YEQz0dDM`1EtXl+o2dhs2V$WL=*1Z z7EbS~8c_x;s5eP0CS9Gorwjm4@$MyF=!_#Sy3+Rk-8Lc%UD++lq(&qaKqh*^RQ+!8 z%LD{^@N9X$QK$Lqm&w5ey%ovjiz~-2!+HHm#eg9v4_{P}kJl^6jskdOVa8Gzb^DPZ zUvjY};l;x#K6@$@_0Oh<0_6(s-$ipdAuvAxK-R>mlzAv-91P23=`hEDGwl+p=?9&< zc&*QH%DgGP2Kge%LHAbzMzD}2VBTOz%C-_&zwy@ngm0ogPoz)J{-crfBjcFKn0%c| zH6@AxIEwjOD51cYbwaUj9@l|qzllh2?UTBa!;$yIlsc7-Zai3so;oVQAt<o?7+5oP z#2!a6wd_KI7q+x!JOUr<n}XhtO>oF~2%5X~9+i=tR?Q9NO43b~36;^O8*sQCf~ep< zr(e?Fk}`;|y+8B^ARP~&odIqHN>wm$>p;6?c*UULHE8YRngcm4L!wa#gme(iC{DaG zS}Y=_kK>px?wq(dLczz*LIKDlE)-*>z^@>??nD|mF?9S!1Hb=V#8#)e(%*C67oEl! zLA{zQN$B3)X8#x*<45BaKtVKbadaU)1_*{KP=GDJ58_K`bUE>2v^;3MXHZFI=0695 z8%BuGcw$o0u=KS)f(?SNDH$oc6UiQy-?5sK^DjiX@{#!$YpmU(>;<DgBOnZ@Q<KP- z(r5#1ZG?<Ad+>Y=9SJ(tBr|y~Xa!9I)N&6Ye@r9%8LXLk`1uJ<Rj28tNJi)pY;0|l z(edvv=xuYu63bgK@~BY9d!}t^jt=~4Mh)xn6r5M`M{=~w+On`zd4!Z**@;c=S&V24 z@02mlx^<%uH-SZd`opQ&Rb$K>hO1&HC-l#D_}}I!o*adg)Y6Pa1F_H{Jw+y^ZRDcD z(tW`M9z6|n5fhx31Dj4g#3my~;$3{PjXP$m$Iu^B;%9(07L&yz6@cdgYgSj?;IUQA zZ5%br`w2P|e-Ks0)c%D20XV$0w3Hs|^&y-LP439Nl(te7ei<|}w{*niU}Kto+F>DK z8;u-%?8<@jX{SD!;{<ecGxl<Ls|GmnGE1%j0n(59up2YUXP|#Wek-TqL^$+5;9|P0 zM+V<0)AkC^$=cx#gi445C7QkoI$gETZgt3&2eInBCsAl2$;qb4i&|I{B(RM2X{$s0 z?I(Q({>uw0inJl^>1pE-_#f_{>0I#v(Qemj>Ru(M)<!y5!@AVCBh#0J*1ADT6obtC z5Pu`99rr}*0I^NUwu~^J;4lPt<zO`igxKo>I!66F3pig?L2oiz22Hzi9h&zt{Ctru zK|LkkXjj;Dj2BINIp1&-6%|}tz^O<je{y03@yM}%zpY%t3z8ZDiW6>1uSUotEt!vr zKU_76k;WjYNJ>#dNrhr!P~__6rGaUBkbS(=YxwUuf<Pr*{DtZ%N$x7D+cDpjIhK=@ z2Vf$aVm#&3RcUGPz-t7BHv(uOq*6JgQz|8;sVecflEVKNV_zK><+}DebT`u7B@%-) z4Bd?q(j^EANC*M~($Xa@gQTdGNGjbWf*>FwNUBH+BH_8&d+q&w=bv+(_0L-Ca-4Z* z-sgRu`~KD4Klc;SNG>a`mGkld=QhFbXcXfo4sHBG*DWxe=iLZn>8%icLob`r^hIS( zHpCgt-8D{>hf*;va;VcgY(M|eP8?p=3oQjI9C~GOryBEt97C-HEjDGU>lDgpZ`?TP zb8^?;t8i8@XIY8eDnpdf+#SLAfhVdD>}Yp)_jC4fVACj6C_H;ry(Y74ALK092U|>~ z#Q(f~04S_UC4MFnn}J4HrDFo1XrQ#X(!b{G67F*<tDtL3h~I`obs2a(*5ln?ClZw- zRiVU^Me^0lig-+Jsai7p>X$zJaGqb?93Hj~38XA?e8RhP?$tTXq2DIB<p_25QrkXZ z=hl)h%(ve2B)b5Rzcs;!&OTA~EfmA&{Q$+RZQ83o^H%?t#jXfA)UAVmGECyO#lNc! zb1@d(jollu!K5eZ0etwP=ewf=Dmy`W`2zCfLhtCBD4$8WrG>R7T>@q4q}Y3uKPZ%I z*igeOPBLe|szZB|xgSu_YXzuc<TEA-&*Dhnb6<!FcXc%LzNUINR%)lEFT04<N!aJs z55<yW5FiuEv@~}RT6ZJqz9yA4&d*!TeiT+@i3$GltYU_mMc=_|zN5#BDk4&_W=rfI z$>4MV*YRSJwg5ygbo>B^%!u-s_veHF<@#pteI6q5uVffb;1jrTVW`Dj=DEGBHck;t zW(L|f9)c$;ct5yR2WWdJC@9ErXb$ruZy2@#x7t+5etR5~kF!dDk}Dt}Go#a*UnTlN z$~6*(i7j7&^<-p@ML3CJkGnc-98WKtb-wt#{aiaEVxC-c2_;ekDH;^J+1}XlO|+~e zG0{yssnO$vUE1MLJsaaboo(@R$Db!tJVnKu#<X^i#WjMgufz+2d0s{IssOx+f8_th z8aAqF6&3S=hd4y|<>MKjTknap#&y@+Xy(;p6+MCG*rDP3d*`YuAQj^Wi<{nK2<vax zh#xQ*$KUQf$G(}g<g?e|V!j4J0Xk|9620ToH*3Ly2i+}>|E0o|j#xCo59}bppQp$B z^oGsv#O|(W_NAV8EfUB=@thPF*Es-*AP+xl$M;Y<IUv*G=`faRt~C?cp<>fNt)y7a z<jHZX93t#08W--z+r*38@r~$Xa#Vj7z8q!U=|0Nh+W)pcQpxL5HmiW>tryRBkR}K` zb+(DBtc({Htu(AIVd}~B5g7Cmr%W=jfffQ*P3=F_)k$uu2z~CH8l26i&>m&Go3Ou( zyY>lpglMVT0RJ=HBMHhuZ+9mrIQFDb=g@(Pwt2iog{DcpVXDne;v1#rY#gHQR-?z% z<Z`q>uy@^%JurLHRqN3E=-xsgo_3hdD*=2WqHe>xDykjEk<OJK?|Z_%O@lpv8U&}2 zy%c$uuN}>#Y(iMzzCl6Pr!OH#*1=dRMhT+IJeR&P_!wRtmEH2$5W(LX(iD9~o)jeB zmVn8SJH<&FUH)D+KYZ%Nadf`AQMPwX>Z`9$iM(6=mrU!q)A^7{>g2?@#9qTDR#9v` z75X7Z?m^c_XpPV0uCwlHt{o>i1OZ+^-$&^zWsEpEQ8<@~XgzexDJb|1>UbBnB|2ci z<yq48t-7yxyO&}2>$1BDJ#~5+w*%aY7h7#+*;@jnK5@9do{l5UWt%s2mDc5D?p|T2 zzo+}});K$rABd`4<**dR?zz8D#p;PUsv*`mJDvX~w0-kWN@183O}M|+Bh-duNzC^i zG36x{+o+3(7Q{`*>G=Ic>v8-&Cbt`Ht86!Yf~9z4*c?60*rvQQT|2JHk(cRMlX}@T z>m+{k9tp&Z^}25U(c*`juV(l9uGU@Y-X6#8Oz&4b!b75FS`*X}S&Kg_xK5mJkNrs} z@-35|UFZ}25&R+SE4jG+f>VeT{rce0p}&;x;`hPRU}>$(LufK<6;>D4NxZ6NuO;Q# z)RAk{VHS6ZKWm6eU&S<%{UpJo?O>jvLG3&=G%U(S!D2F?U&+z<k@hk}%$jfbBV?j( zARl3rTa<Vg1BryR-ydW#PlH2l@ugeCI~aw%Oh*cm+(rrK4$px@o6Vbh=J(+<qxz%+ zDmtudK<T%9LHwhVlL?OLV&ZiQL3Q-WUaP^UPD7gqPH{~84`>7?u51YLloEL3S}I}E zq~nE%%pdfgPlP#Ca2*yI%?-6H`s4EM?N#te()I}Uc&?t1&t=-o7__F_GCjh3DsZiW zFP!#oW7JGE<!4aYrl4#$9*)rF6ZSg&XqUT%^>RbjKV9@Gt;-`(lN!1f`eS^hwu~YI zb=4O?o8}poh*Qq&;DmdQ&y>kDdY9;zVcePs#cg(M+f`x*HSV&^+V<rK%bky?bM`V& zwe9Aa;)nC=Tn^tDuByrxW9hMx$nPRkmz%y3NZ)OZ@h>G3iyP^o7%oPLd1GPMi7X?Q zrsNoHD4gH~koQ^Bm9|qN3*^q|a9))a0@qpbW}naem%xrp?|ah)FRtt^@|%<e1L?Pb zO-pwE!}jm3Ey4t5+38>sCx6vru5mCF`p@qibrD?|FjXowKC}0DS!>^oE6TZx6cweQ zO?9T+y#tu?9v24xH$5}~dM{B#RZgBWf{#-%siDyeRrXA+URz76h}|N4(1(EOV{7E) zYgiy?EXS$28KicrLuT?6-wn8LC_x+_&$eSdXBiQ5dBkKwL1@D}ry$LiePPC3rVoBp zP64}rYKD@q$l#e<B>BdlY31D}BT4KQ;NJi|H>H!me*nFly_#MwGA=18TKw0~Pxn>^ zQj_v7FPZ}o{sifkCg*7U1hX4sJ}Vu*?H&1>V@%zHDR02lE3F;?hqXELQ{M{_&hj$w z)!M}`y4b(Mn*!+ebOvY_`yF4G^|1m5FL$!z+~nQF`1sZbXEJy*4K+37ZNq=g2i_Oo zm5tGPksG-wrk;YRa&dMBd2h4_<f|ksLK{1@SKw_wie(nfQN!dg{_&-)!NY{u*p#y^ zK<940HyDiUQ9b!qq5#QdyY+U!&H~K!+H;jKHWHGTc*-btr>kvkCvQfG6`8k*wP(9t zbhVS`=eN|zIU!8y_|c`o!9m8W9-0OQ)t-JV%%B*92KI0UvjqSChgl!AprGI?FVR#6 zzo6hNi@MFI%w@c{d;JRAyE?hwGi$jwOr*ZkutZ$T`zS$wkNroGY_hbYP4!~(o&Snb zsSSSQ;a?&w1SI+2feinDp=yjmTj1~qaG7_-9`y5<_?CC;(s*gW)&6+>wLJBnT@xUy zP?5Bp1#-}+)CFo3JiUJrROfA^BRw;}&e+UK^4j|ZFwueH92QeVX!j46V_r$~%{rpO z_75!(1);c2Z^-BIPokTo?tuG<gQFwhPK&!~-|u6{Jy@;0ws!o)K#G!*a`U6TDVES1 zkddwqXOS^p)qPTgz4~h(uWd?Yp_Ic@<%N$3li$-4yp^I4e!|nN<AB;{170@0_RZLE z5FjtG-B(xDS0;X<rKS$v8{|FwbW^V54Sc`WenFNq&;ysgdD^?qAS)(jykJ~q_?zrM z>1J{81uwAUp%9BT=^v%MzhWVwAsh>Dx{>C=MY!9=ohs)d#_f9p_T82S?`8ES{Dhtd znd-MiKqR=2yWP?Y3-D<Bi|x7tsCC}}Y&fUc;Rt#+I6%0nmB25X*;sb6|Av46d&C@{ zoy~@|Sw-Tf_VYhKShnkSJHYW*n3Hqt+QocyTLXIfgX7EnPvnDIe824tq+KGRf}4C1 z*+@bql5C)GT$0=G3bNpR_{e0FQW@THNP-Kf&-?L0xU#u+{kI+(f+%!F*~jVL)YQ~e zZFNED!N;EqPe5!eopPi%Z9E6<MxKN_&x!q5q2d=%2E)s4$>oUcJyZv&a1_+JCJ?*x zmBO$P+j4=;(xk=2$jEyfr+;>K7Mmh4m49|Obz4-#ZM3!Z35uM<#Kgp4WmP`90be-b zY_gTg2+SuK8Te}ytgKf&qL-mA;Fag>mOEfpr-TSJDyj&@?z_q=Dxu(PA}o}>NBS@= zGxPQBPghTwJniq^UU(y|s93lRxN2!>>9y<E*H%|MO{Heu21%yAuOB`JtXNG$Lq!JA z`__In5Ep1_YU%mSBQ7yA=H=(Vf&51@9`6I___ya?>u8dZu;&YR{2Y%g6Mt0)IgmBC zrUclBc=z{>(i%vaWlRkX$02pWvS@ijrF~*@vN>`Nv}dTUUY#rs4i4d_YG)<M`-x66 z55PBW^Y%W)mq^v8mkBq4z+e5wjF*uRAODH~X&qKZu1~@Dern@yzv4N?>r_tcmXZ=D zNC{EhgTx3lqjUu61jS=PrfN_E{9=;)01;^gZETOf=V$fmDl02PKu#)I_&J35J!^t} z`_GT2>TBi6=5de4ed|jJ*N+DR(`YI=ju+zVIXLO>ALl;VLcdu;b3}%BbB|vbtoh=| zvX3I+sR5m~jHWf|3YY#vC+xY_NPdFbrL^@)A4P5I1+P1+HUP%F>lQ!8^YgRdHC24) z;56%uaM~eJr!AlZvYbrsF5@PtZm+1l#I&jhWC|`_?%w$ua#t$`MGgFro4U0IkneZ- zuC4(+wJ!5^m(BX3=M46;L4nLQeB@bI^@i-7<ZB?r4DULK-OaS7B9_4lZ(L!KjlOLp zap$A(Y09IiCi$xxq_6q-s|VD%eb<PKh2qpV6ZWBF;zw!as%yRA<&Hve;u+X6aHJO% z$@~yLcfg>wlX1LxkYz?8y~Q}YbsT6+c^9m01|%7$2a^Ul-gH!-T`}32VnyF-=oNW= ze}tkE!_SatAycyA=@GdRD>c>rpce}}s-%RAgVrSeTlnJ<rm;e~z&+>-MSHW&B9#@L z#g6fa=@#2bSvvgd_0uXwueN?FEuA4}SH&3Vs#X07fa^eO;7>PQy3*z3`RfvBB7*_p z35^KKjp#54$O2l3vz1l)F7`Bz8ryJ#>-(pWiV}SE)_ami4CxWJqV4)n{#~IWYro6g zkeyCED@zcr#l3+QuwtZ%DpUvo94Cf^k>}_yk8ZXIoOUHjR6pg}!5lR*Fc8M5bkkm+ z95jb>-qGA3#vbKI33}o4h;iTMQCb8U=m-RgEcH9N2OxTIqO@k9WLHtio&+Mxj!xXl zuoOr-Y)KaxUCd~0ytn`n%BP(gtRbpj;w&S#vuy6i=~F~qV5Id}cx=_0Wg1@i_eYNb z?<-=tWM2gyR1L}dc<<018}ExnDW+=(kh@HyMxc{#zTkS+?eoc9IVdqq@R7IRAZD1x zZ2be0Ggq@nNA)uH+7LL@smo-YXkEEa6ZXN3o+`^W^F7$UC{=L=2dCx~bgrW35Ko{{ z->;4TZgf=5fG&C2_!v?p$7y<E1uA4h{`lT+aT=ubEz_vazV|l?OWRm|;XE-l^mJE4 zJ>LOopBqiC@=^rf5uzDQPO;fRS0fX&#q>gyi>t@WHi1)pkyyD#<o;LO84MSK&|Xoy zsiw!-8OWHIhJH(3Q9_z;3g~~l{WX0f$U5bt!ufB<KH6)w?3?uN<1K?PiG`n#d9NTa z&-xfW?X+@#fCj4gRY*VShQu`5y~N8m%v%}bn<uDfz33+SmXk!;lbXnjqwC=aP47jo z5wjM@krfjZl>WKsJZ<b)-qUVIIQX&><acU+d2n+1*>|No$-l~UbCgxaP4elFfO$Rx z?owpa%r*pK<o3Qc9z=aRg<6gdbJX?e#7K_#H&V*Q-LVjeEt!c-gNkES?rjj5(KL^A zZ>Z<kT)ELeEMb^Np(=JO83fnK#8l}6<gP)tM7SxsL~n@{dxrs;!X7z&b2K-KqB36N zQS>8l8+_o(p5FLKtSk41Df#C2X-!9-k{1$Ma!lkFD^IAzCwayr!+>16wPM)OR=qRu zMP~3P$(q;sGZWn|4XUqAw#nzv_3R*&FZHqS;>XLUW@!wtVS~5PLjaj-4hcleY~vrJ zQN_l3{q_Hn3x&-eU+7?-YiX@Nzg~TKF2g8xFvwd7LAF+R{KcA`f)M9g3_K=ZOEY+* z4Jj$HxVe+lqAxOfvXM+l-ywr8bL4i1Q{-xgtNjx6k(^tdSm1L}Vq!ZpK|?f;<}#w; zmd2$n=HmtLte&(JcBZ70DGT7vQ&6pSMKrlMFV_(Qxb5uYslD$LNr&E|CZvN0ytgNI zk3fl}&gSh1a!7L5SQh}0_(RYB_$4uC`45zYrJ`Q4#n+SeMoKF=Gab>O_0X&85@j9u zwhxX=r44*S|4bIHnByby%(ec+pHFbgkp8+i?>X`;$pJt}a@!%5DCR85VgbjP)!Uzx zeJpfhaW*im64zF$dQ#X`=$yOIU~)jwvl}OH24{70TQ8A$DNO(MC4c+|H2R!5$%cYP zlr+wa7Kr9ZjQHmr+tTK>4mhWS-kycsFV%d=I1O<@fNmWzp02lqmO|uCwgOzeWLF={ zWcn}$=&SipIkgL|+if=%ALdViIJ24e4+!;Y7SJ5>uba0o@?yU~_!$1q#3(pf-NIC) z?I1(IRn2GIb}W7kIO+_0N|E`AS&O2gCXBf)9sTG~aU`H8)cr2+;r9Pp^vG9|vWZMJ ziYEQ~*HCxPL9YvQPw9?<8WUZz8U`hI(j(6r#3H41k``Fc(CO!kWbfpU@*#7;Oi(~3 z*Kr6HC0a)AN}5t5sK9cC@?^PBD9k(H^l@89S}#e;Nd8myU7rC_q1p2XQ@SJqUl9Qc z%fqVad6QxmY`cF>l%otjkc2DvZjjUWdf(%yBjt&$Aw)Jh=G+}|5>A!Yz2qW0teyQ- zgv9|vD_&X!yNHf;G?!5ts_l#{jl&cQf>zyO@U5o|5);s1>dmp;;(Qdy_vI%?3jTl= z7dkGuwwEizb3R*#x>zk|f8~P}t*e^dQ(INmjOLApdkeA)6-hkp%|2;0%1ugAq0a;f zi`idJ;_&c>rm9O)JQ&8gog<u5JFq(v>Z`A*nNnRCdp-N%h|<kx!qLw69g&q(UHCSd zWy>1Ybfn>88*E*x!`hXT%t9&~s@%t<w5{yv;Ue-6Q92mUbOMvylDZCv&fc}4@UR^| z5&sDCF(Mc>B6$5C&s}khg#9A^V5g=1{8x1n;|(_M5_>=$s2#h~WbONOZSqi!AQW%( zwek#qAZj9omdV=jnxTB3ARywn5$k1$HK9maSXk(Zk1UG#9+?D^^e344S+@zurSh~; z98oADC`%#-DK4u~>k7`55{;K_x1Ef6Z>M<c#;@G<g__5sFP0Kh4;zJv&G4(@PW5vV z9TcfYUw%v`?BO50iy;&tryV(TP+oK9n*?C)Y*0rUR?BjeJ60vtP5eA;r0+RUYu~8n z&cXf3RTj88auuLb3GCb#N*`C!66)B;tsYJ-;c7ajVp*t)28Wf#zUA!!`8JC>4Y^CJ z-wz&JiH<IBGCDf?A#Yka;*?{WuS(bqRecyq?Dw3${Q{E7<hl(h$?Mx&B!xGm@ITa` zg3{x{_0YZRyYiZf*62F{0ZsgIndoH)CsNR1-V}a&IOks18eIY!;`8N02-kuR)1o2H zak#P0sTN-0h;5~OI3=}pcBL#+UV>%iaK(L=<DvPV-f_k=dTQf~7$JK1oaPZWGR%<0 z4h&IGNG=-?Q$|Y%dfKlkC_4eU^VA6raQnX_JPK`a@{EL}s?3Q!&@X$njgoEW9k0V< z*bBS%jUVfX%X8CIj-bL3Q!k8jgrfE5m#*b_vFAA=Yr+z|tHVk>SmLa_kH@vNvcJNS zGis%$t)2GSQPYJ%LsfM^HL7B8Qw%c>Co&=Z`1snLKTP5-3Dm7e?z?qj*ZXu|0q8h% z<cri8%25cB;T~lD68cwu|4}-ZJ`X$Iic1xxZ|pgxL#&i;KysV-b0ZJ1*FAG{t8-yv z$`|HC@kFI+Ns0;^rKyQiXxvGUiHho66wL^64_~0DBPgKe!8XVi!C+odXrZI|5oa&0 z)h=a_k%iZkRv%kVr&O1_Oo^k3MH$;wvECGzx^YGEj6F~#)Gf9rnrh~g^@fC_0aMXs z)34PY{8T0>D@~E)zINeB%^R_2B(wrrnz`aPNzJ5rqH({{8$<uy!X7q13wj1Q)==op zuSiejq26VSs)%uaqsn57_`0tvn1@<^2Sx=B@33aKs~TyQtHUA6B%9-RvYd!L?J)0r zsl{6RnT{I1gTch|w=oCXhvaarX(UWbBbJk8FVzjl2>nK~QHA8bz&#p^Btk6?*8p-? zWU4In3^u6ePc~n@!^gR{)!od8z(`~6=y?iM#Uu?fYA=%dcg!YZ0|D~29hvIYgKCO! znlnh<TzV)W&6CC~*0mn1NzR{a4~YeQ^8iohjO{3@=R~8M>k1I=D6}-6uVlE$tr9GU zHf-~**Kc>%DDaViktJc?UYGCNdp-^LR+=@AaWaF=5XEc@#8tQ2@jz2eL+2i!+h0>s zys)W3Q67iQ#oI`{!cT958kft=f}|sZ^YNAgs9X=0Dwl=dHd&H{N0SU!BtogM8J%2& zb?aTeG?gs;dA>j7qU>(r`U;5gUF`IDb{YJC*&55Iq01bnF^1F*pAqz;C09HgGl3wS z@fpF4tN%LxrK@u$j!~sJ3Z+B;MkhM%1OIwz-2F6kZ|&0TRp=tS^HAazk;>%Us`gP` zSbhfRRi!7o#QVjPE>gU4%pd`2^rVAahW4(7So}StHh%r7%R?{J277HPK3rLM4!A5S zIDpb=l6_^ibpmk|n0Wb_hB~}%NrAmxS?%oX>`8gFahdfTH^>O^Kt<Ne5v(e7-HvBC z;s<*cOGx>>yY4g%geEH^P>praD-$y##p0ZG9XXhdR303mY_b>!8<6!~n0L4306^{@ zlIQ(YvlHZrnb_4M!6!C5?19yLJ)2pOU3&{j<tx>_L76Q@*2puCE*F}|A;ym8aOz$g zJgDf=x|RBTJQ#{zV`66WT_3jgPO&MF-zPj2O-#Em4Kv~o_`IlxRD>m&015qpp4!jZ zA{<{nY09ySrr+c5!1RO4eecti{zU7NYuV|68Fa6V>zdx(wgW>UBsECCTjBUc*;J9F zo0GPeIX9L~_xMa#U3hkuK@Z<GlC3$pBcFq&Gx~j`Lxlzn?+ulk(q(>Y&*bJU+oCkC zRhle9U`)!TNsBS6c{be>Ymu{WvGdU>g$M@@1_z!A^8AEZ?ITf!TjY$e<*nWeif--T z&HjDxFjmQaUsm~m_YHaL_~R;e+XoO-#Qylv`3E)oDsA;n?HQWm$SX8UQAhXWhR3Ww z)o@VB6Qr4wxnYw;8bYG5{v9M*@$4xvbBTy}0e-Z({t%h`H|qWmy$y%S`1}zC?@3zi zZ9HO`Q|uOSx_6cou|Gkh)j_mM%Y)Rm<x6I0sNDM`c09|}fuFC$5dGBFH^+`By^obE z&F7LJ-~vmGd@3B`f#lruCNBiqVxrubKhvo)sNHDU-aVi*1tLJrU^j3+bGJ9h8&M*P z&DRm@rK>jf%D5{{Y-0_VrprQ8<WDxk@;HthFH|I*rJNg{xJC_&I_juvEAf?kf;<yx z2fIpluCR1%3~US0izz%kPRx^ZZlX6~aQ-mPDw}Pq`&3|9<d5>@gA%%9TE%kV{!}BQ z{QkKOep%d{gt}Ur?-TsLTN0N4-rSa(uWOn(|J&&CAn)G2FQ2*cB>f~?ji?+cCnb1l zdn$|^J>E+k+Bk0F2v)>Slsq>#{D5Ca!=Z;wAtIosBos+AbgD4&_DO*p83&uehnSO~ zz`#*)pP`T~pLhB1=*LD<Sh{0<#;BR@jy@Q_?xat0AFmsqSEc}&q0Zj_;CJUvwDxLM z5}vC?<qvFE2Q!zyy2hU|ax<(Nn&t-zu&VI)c4CAYU0prqAyleL-gH1k@$#4OwU#UI zh@~+s(+E%|6dx7F%&YkKyyBd(K#f&QUkXaD?uIZijoK6niYp?oUvD-y$)biSDTZ0@ z{YyqEbRw--XTl@vMs$0=))CM7ZYjdXZ5D(|&DXD<{=v|$IuUDh$s@HJVmG-}?wE(; zT47d2G^QclEF(gD6R|v351n6?GqO+`ca;b=DO078HnPHjF^G>}XBY9pedSM=bJUi2 zXmlTL8TQ=@i!hgHwz0@pqm`~z+9VC%J%&P4su4&*9M{b6@69w_H@dGX{nFGywEU{3 zi(!)4Nq;18&HaNYLhqJ&N+8Rmb56DRphD2$i+L=>?TF12nv@)i4>*RHJtL>t5@}KN zuiD!cKMcEdP){DyhL+8ZP~>EbMMZVnN$}C@W5(t@%bRLMokE&vr^HYZvVwxaWkOf% zfdl2KMk&bgJ`6X^GZa;P_+WY%3Q{jREE9)!?V!NT${C#1)aKcxt8wTh^x&kQx3IJX z5v0Gk5$@4yCL2Q72iMCVga_2z?=XkI+?ee%&B0AUu@rS&+6tvhqum;~s-NN+GI%-I zVlUj0Ke4UI+LuKyJtlURQ}WZ^&reu?zkU95KM?y7NnptuT-yP)#iFOc!w`sScP&1B zX&BkK`k9BTgXKl3-v=94cMzx7kz3iAL}hJ+1a?{1Ii{UP?I!R^=UD14!3OKZ^<ki^ zM`g?Y9-10`ty06n%7EsI&5LRY!`DK6e`+23#Wubi@x;GLFtEK^{^#c}#<zGatF%*} zk0ku}jQdDyBZ(waNb*<=G*hx%4C^Kl&1!SX1WY(Y)@}Z{a}DE!A|EWunmLds&HOiq zriGZV9@X%Vo>?y2m@QW`Z#~i3BL67Elyr9c{uEmD(_61V3%AMrc>mn#V6W$T#$nf= zu{dXDW#y5jR+t`5fI$;YzWBSXI;X{FU0+>YJ%)^1U@BzlTbn8!6_pR@F2NyTxnU(b zdq3QD>|<O73qL>Vxv4;=cIjTd3^U5i)Kuig=*WnWfWWOu>uXbv?68WoOO2nIKDl0w z<(hd<2gCB>{{%A2OA#;k1penQ!!KjQjpKUl#1}p!zPWiT`RCfoN>gw`jq^1#Gnk9> z&5;s5X7w{%a$qLB`e#b8Jv$PK6cHI;H8eJ!fYio`KTl&jz&0`{IC1UbwJ-ns+VKl+ zV*`w=t?hb`3xK2cG;sqm#PXGkUcW^-Tu`NkKH<1PbsA><kQ$ivrC`#`u-l)5Z|S+Y z-{;e_v(3%TUH^TtmD2F<H`;{b1xlIMu61^Hf>yJ(ww8W<46bt<-Wqi2R<9sXw6e0Y zi<9GNaq(<7w+FyJfIR3K7)Z|k`2HOvt!;o{ot+$g{P^)po6mZp<igI;5v@amhwJ%9 zaWddl*Nlxh5CsJV%rrDSU&y)pAh3e()Xpk@AIkt329Pyb$v`LrA!?X5D+%&TSy*f! z8a6%s72LL_`8u-%!`9<Rl^4G}i|l`{R}}_#oIuyW{{AA3*SuK_iOeXt2=yF#4Zx?O zqT+);fBx+K1(W%Or}E|kXm31cYKm%WDHaOEoC$+ER-rz~!Df>R61Hlxn-w_&0Fida z;|#DCYFgUvPSa5XeBcEpR=}~?E_Vmi?I=V`MTINK);nZq#_}YSzyA(}hw|~6^bc4@ z$tRi@OMxXFe)H!gh|gNssF=t|^TU|n7-IUbukJ7-<$N~`Lqq?9Q2OSRKo^id?hU%$ z3yXG#I^gn&{tgJ8?|ddh+Xk*)n*9Q5;1@4mKw?!Qy?FF@xV13n#aLQ(QGOSM0Zu^x zKi5SrgFH4xCPCrBX#l0+XPql=P0eh{0)rM*{<VHD9!^+-8~PjwOSY!Ey1O4Lj3DII zLlTK3M*snLSx5*Hoq|AGIeWOf>y5DiY=?}DOfE^4_x79pBtONltL|uMtsWN?kg!(Y zXw*OaFg-n_Zf*UU^?V1<b)n8P66?a&0y0PpwcE3GYKUAFwsV*~_3oy##Uar)cGJb8 z{WYV7VYzzEIBbi!vbq|^X`tXAJv}}6&W2t)pn`z-YhPHH5QhnOOxUKkw-?kp;8`Oe zC522~F|ONn(8mBUl%D|#LJrZ7Z9sxt%svrvjgCG!+7A7_V=tTpVy2CWQX;pZ;o*z; zATD0s{@&h1CTTHANm|me;bHM!{ahr!g2I#gFcXHju24&VYw9`esl4BJQ?I|!%{>k= z_ycRU!=(X+_atDnzg)lA1Exg(dEjIt7S}2&_z-4WzxSw46B83*OYm@YMckfMfJsPl zQs$PHL+^k~NoPJ+2>kv5w!lSe^FvVff;c`dF3t*Dk3lnQ4~$MYqCi4iWNU+D9zY?W zCb6mq+kil-${vfs7ma?T|3pq}DlID!`m(#+iE(kMYVY8RAl0L|#6VlSW&4NH#fANH z)&BR7dR;!3LrhRW0KS6Rv*D}oXWw_T{=NQQ-bZFWc>>qBQE<mr#uWCbsbxOcz0BZ# z{Bo^qYdeR_IQ?5gVg!G54Nvo*H@w)(v{&8!dB)(sVNJ_j{^#%gKexJn_Ql8leEh$5 z#i7anY+3($r$EbpHl=@Up#lc5dtNM!>;HVHylxmor_2LylbZUz%nDdDVgdqtPI*p8 z!1$hs>iuehul>(8Or;{MuU+#4aE>so5SGhDm?L{gu`@8Y!~Ol`dNf&&A1lBU?Ey(P z$B+c+X=0Q4@qc#fDN)4d@$njQk@0u$u5fa8fBblFw$AzW>(?Mi0=c}0^fKs_<_O1O z?<5a{sYsQW2cB2%AHwhKReAcNx_X`0r@T*{N0sKUc(;8-K$?6-Sy{AGT?j-=G(3O) z-18G-UHSxUk}B+FBaa?E8XD3z;nA3NB=k^%U*O+A>dPdI4Pfda6+;<K*f9qR=xcrw zj49;f-SCVtr&wRV?ox-+VN0b#9~1CeTooa>8*brL@A33uo^z{3*c$n~OPKB|I|K~o z%LhG<Wxu|m_mD?H;T%?-h`YW%DS@{3#qbC42ywfZOxQLzHv^##(Z^-Z2KA66q^0GN z8UjCS4{oX#h)aRcg>rjkbv=Cz2qe>t++2tnQLT#b_V$%=W&wXd8;!vK9E`YVU`WWB znQuQ(R;$?tsH6Dl(;0I}(}Fmn@FKv{A|s=_`oZC}|LYfI@2mj1LV)Vlz0u$S;UDOf z#YH<hSJ=T|CXLIx%4^TTO?5H2$`nrPs+yW{W;A~w=<^}rra&R2B`kMH<>|c@1<|)& zD+3f>^zY0*e%KM`=8igp85ZS|IYjhfWg3UuU#^^8c!USCkq7*W5f2|Ot^sA@<P19z z@gtV<Q#cl=u9jq=zm_`}OWXU{5?w-H>`tY|ftJiz<B)7!H!tK%xPTKsi%5kqBO6;= zgQAP%1ui6zx!QO&xR8DD9<onDzykq1xXZgQf6wpXdqlqkcUWX3<b?8|K4rXua}K^k z3l`GfM0W8P1S#P6J(DYn?o(e=t{WBN6B0_up`^-#c`uOmy^|%3o5YP_eFb4{V&b#2 zsD!bxx#^K6G2@`PbEac$Z5|+H?KZo~NJkg2KE`~kqYM_vw^vG?U0f*T#t%ahP5=AY z_z_=BlM=srb=tX{3Qc?2vZoIM%NBqWfGz2qb|?qwMyUa5okzR<to{A{g))A(fb&3K zI=BFqhi%^M0uF3A2I(ge5fMtF_rh><F=XksAS1YKyVCtyD-KFLMpO}F10)ZH@<B8$ zIKkhhySlo74ID7K7q-TG=KffsP*WSW-t@ZzuF{CypI~Ym&)+LW)&b{E&hAoh@U{f; z@}K?SJ6%{(LIRUQaDa+Rg!8NSz^EhGcu`<=nr|T*7n#0U7{U|!npM;RTcc<JvO+G= z(!SF8O%S%wa66A$0W1)7!!0KwIYcNkGc!nqz7I<{Gcyy)6tl3y3IHj+nw+sN40ZSv z&8x&1;zZg|q73{X9fuYXRpT7-dp08@EsYX0THjZA30*v>Mmw2o$`zWyMpHThZ$fmd zk?nsfyqLFrciul$Lwi|TS_TCC21|+^+j*g+r%hm>G*S)-2PlaE8wYLQDm-<dcd!CP zUhWKjW`i8DC>4<XS=Bjd!9|&xQd>#*Zj1x3KBN+3N8iCrOG`^Zp96?%e*S5>;u-%H zMb@yO@6XB?Yk=lXWL0Dd3j*)VGwUY3Rf6~TnwY<r%7PK87dPD4)D(Lg81RC3Lurg7 zOmj8%5gl7lEC4$#rR_0r=J?eEPL&`KUV{3bSvwT2kN7$dOSaY2^fWFG9u@A$5X6q? z|CyOF=P@fuP`q2R@TBDR|N7DzZFrUouG2$i_5%^oWAYLb5^Jlj!kEJX+iiR=EhOfX zpPMrCW!(V@&ujHEjZW92bBUmp%fc_~_5QOrYm^r5{kx|9zczi}b<>cLGq_Z6BeFim zz_!uS8Vq5*`_s#a^VjtB^nzrmT#VP3_LYm-mGE;OovCnO;gWh5Ex-?JI2Q8qu92Ug z#3&RTZXq<5s*fA|L#(Z>PcWkspk#m&^7iP={XxkP)}42idrLU?J2#7lYu*%HDlFcs zz`tzyag*rgH;q7i?b7=~uoy2Y^?Ul^R>19hwI!ZQ($J2B!h~5ape725ebZgDQSV_K zu(1BHgP)YWNl6!nWAvnrUy+;J8Ok;Yd>}U`3JwH|Sm@agTaYjuX{Tgo&$NgBJ+WNn z$R^_isA}?$P=Fr<oY%e#(0XHn`VNCaKHIYR*@pjyWHx+!d{_6r>x3`J2ne7+fL$>l z<?c$n3$Uz`L2zC~LnIHng}2P;<?)z%jHZnp$UcmZR5r8BrYlX8qyDMv#?Y{XvM4@~ zgL!S>*0(B$%GvUJz|VKx6A;3NDx{$d`v_GAv&}ZpyuPr@PnTRINFuf#PeRn)w@CxV zRj#hVl&8b#0_qw2Kz%hBR`2W7-kyLFfJ!k9{6uF^%8xZ%uZMlje?_^Y@Qv9J6^An9 zHH9DLs*PNYTPW%;2*SU#|CFm3OqHx)5jewm?*0~DzRDF;48n}jLdEtF=dRWM+k<*X zBO|)n>5`?DrYsk2$wi4wUk}5VAS?}%K0ddIs`q{JGz~t$VDR@orX$~cct#em|4s!l z`pneaoUdW&HkJ)K?iU><vWjLnkwq`erpe3R&dx4|dP+O&j;m|f6WN`g*Dmfw?!7oU zjPYN;enEK;di1K()bfgojeG4U*0MAd&rDzbQy_h&@7Zvj#R`S~2E;K<0p6Fe&`@g; zH-b?S<~j}6d1{xImxCFw&zUKTqIm{(kio)N)mF{Qx%(CtdkXY>Pa3(2a_!@R|4uW{ zMo1w7*@xi<4hLNY;nplDNl8-Z!*Df(*VgK&f*!~P(d^e;g;^z%l4it?yxiE>*uOo$ z2tT(5&v^41O;(2aVf+-k*{vR8!%M%5*`cJFNO_KfbJ`<f@tqBE!S%zv_}uIhlVBbI z^)ivPw1cja5(anr0?x&B#M*r2{3b_4?g2a?$jBBLsu<Vr1?}qxh%l?Rdix#Q^+hT9 z&WC0cl<8Xf5b68o@{{6vitd#*^qZ{$8vCoHoj;K&b8xP+Ax7Evz6Sp(t!&9H&WP)r zXx7IV4@m10#p0~JEO9x}O4024gqHxvF!1$_AmbG4&$avG4u}1oz<rC^hpYp!*Tw=# z&r8DT;<za(anarscREFye`v|o=v7G3?$J=w7_;$9`yTH($4N3NzpdLTD=CR|DVxPz z<`}pOMOdBX8EE;Nhzd1F(q9Z(=j3D<d^F_Y-ch9IE)DK~L{FyZZE9sT{}y5CU?Y$@ zQQ|gc5@DW6{*+*|D0v_=Emz>Nqkw+*+p*uBW+Siv9Bmtfmw3t$3ET8|5Cm~zjsGSw zBUk&1!f?ISIW;kCt}|sSAm2rh>DD?Hyf^&}y6up^XGe9T76hna2Cn?*g5T|B#k7Xc zWSal;(7}7`QH5P3e|R;(EdeLoV|mCjd>jPn)FY|Mzs(O{PJ%u&#RJ%5H2a5NP<&#E z!d$~0$x@JZKQby*4h{*4<4)=lPS}MG_IY)6OgBP<a&zHr9tpWUJf%>Ame7ki#bUU6 zIG#XgHMetc@F^TV=tSsq2_$#KA+C|1-wou3(NmgQ_p7>AeomGrcaT(I_(7WU@<1wQ z2Z@9-8DbVj?2<I0h-)NFd-B6An{GuYf$H-{jUC3%U%R{Pw7pQx(niCmTDB|<SFy2N z2_i*A>e#0TxM&QA3CF9q|3csfcH9m@Kui`fs@s_Diuza4{a=E^xEmLFJ{?qxVsISe z^Sc~v!QfZoZlerD<3)Fxo%o5}r6pmjWA4JtRCq89+%ieCa^tu_(^bemgal2Ds4c<I z7#lDYM3fc5v3r>+^TNi)<0x{wyqI0C?&Rb|_B~BlnXx5k1#f9rJ+JO(=PEPKb=mr$ zT;VGpRc&7TP)Ot{hU@A6<mzx!fk-+cjCxlb!{x~W7M>dqbL4Yq2Q{Sw(vr%oO|}1A z4AWzHmL*2OJMqHu^`kkca-JorL^6r$9+nqA$i<y_xx$em5}`TM<h=}baxU&7O>Jcu zev}i5jLF4urP?=rv>%6J!1rm*X8@50hr(b~C+maX?94-b7x_t#sU9$u^4&W9*nZBv zy3Ew^7V1`CjQ7L*-`3YPYN4p+s!62B97n>gS=;jt7&`EyI?W>VN{lElcEGo8x3sg= zElr`R^0%(A?1FV17w3I;^5wP^GZ@*^?TD6f(Dru-i~L5+q^}$#Hdc~mVAhxS_@z%a zg#_jVi_0}i(0=3;rm_0ClI%@<32N7dO^Q@^jaL?QW^}!uwDYYpo4T%^hzzM@VfwD8 z%A&gciJx~6c8k=U#3-IF0Tpl>Nj9<^Le8!Mb;Iy1hFv&6af{dB1%k;Fiyd4-j_ZlZ z56Ft>LfzmZtYy4&woCMzOVJ?Wi%~j*F`?-v_I4FM1bF5MJV#svMyN<+9-}9WpnWLR ztQz)Y%sCf9E`;AK3=;W<<%1Ol%%L}+G1{BMOId|8YX=)!eXt&<8_iKhDDvoyUAP5y zuu42a%rz|zgQR|wu_GlB!?kXie!dQpLf0o@ZO)>@O_hGsn7FVZF@#<*=}0X~CmAz8 z)=}e-i}>q?$rtz6O^Azuk&)=jGL2yZGKG6aV{!-g@>AIjlVBXhrN9xi`5FQocHFxl zu(?D|juM<_y<Ku^W4g|>?W-U$F8S&m`OUH3M<iZy0s=!h@l{a6#W{y+HO9RoBwT<r z>|o{61OE9dgH0u<<0j_nub*tce<wzn(E2cvcA{tG03~X{BI>*zZ7?|IN0-*>e<l;N zXemRz(SwzO{216p7FMQCnCg-iy*9vg3!Z-_2eOdJ?p<CgF2a)SGK0b~5A08TkzQ#T zi&yxU)GSqvP)@H9w;i5ne^VXe8i+H<GKs*(9N&?-HeO`D@bZRatra0JiRcb6khyu| zJRWq>1DQ@f;?^QThaQg6`S?=USG;4%OKpa6T?yFN9CgDtt~^RpbPopD$6QB5#I1HS z@9c3N!gc=cER9ML0vY}Rz{Jg!V6OUkvziLipVwnBkf}qOsKtgizrV3RqG6E<ZVItI zTYS+<8?m5cr2$6PRiSVlAHgqodw-=0`hC)^|C<6c6<Mm>512waxdUJ(n%?8ngY*R9 zt*?}QDWSkBKGwmlBVzh8Ugo6!L&GO9=idwS<oMXazX_976*Jg>_p91K^54-fXY|RP z5{q%7jAH;N#$4ChPrqzPa7X+aJWJj4))&~7^s_;HO^bUN3=LtYbT4PI+FB7vKF8V6 za@+w;PXvSc(Xaht|EB&xHB1UP6Y0SZMT5q+i5y{*7D)Rc5YQ<tEE%FFT;U3Y8|X8! zt_bB(yHQ`py!X4X1+Ah*z{UV<!o_0>F?F0w(-6;RVtE6DAvf|@B9@-?&NSGA`*HO; z$cRaNQ>v@0br~T^!hIHT`RCU(9J)9`gti^^yYzSmevSEIE{v?Jj0EK6=22dL>gv%5 zB!#luWd!nd4FxlEV!W3b#99z}D5bCAVth2-ASs-*@MTpeM43oEvFDv66aMAx?JcF+ zMOkz8c{dK){bv~f9q|fJIe4%rQw}naRE?VAB&bR9xnXu<^eZnZ3fF`f>eVO0Fpxt_ z3z<ggzIskn9PrOV62lr7F2CpzYbNUyado%xQOg{)`Oj!cXzwlI?JuZMU6p#oliy%Q zFDjt-L~b^lofmV}PytI&K@@R$P=q)P_u!fS79L~)M-K3oitkx}*rX(F$v)0Q()_L8 zQ}O&aoieJWlYO_#>of0$bJW*j&SQ6mn@NE@WiQhjN1(FAil#!Zu4Y|6J&PTV^tme( z8gVC`F1@M8{E+*`mRTHz)$c8w@z|yPHbmm;!*iE|GnmB#2QZo0uRDm#WE6y`8&`EO zdJ+#A=LxsR5S`S052~>2sCe<cb|5EYNZU|2Fu!3Df29978<KtBbTO?62=fQ-k4j3% zcM?7P0-dInq`kyi--j-#pP1uLOFddTI#}_wlL({)fR1TY)6S|fJQxbiM+AbyLxlP= zbvI#tSBAeRJz}lm!%CXOco_aXZUziR8F?blNat{pak+>}iuhiR3Ef8L;*Cbn?uVDL zkc_4=&jJX<y0NUD5u?OC7H^)8*N4rtRSSt-d;t^mylM$@L<DiUIyl->YA7oAu1)Bf zA0qcbCvo80IlvHm*!QNTTX=YQ`U1~VwJ@!{j)yWSIMp*gH+BB)I{5p$UlAJ(hY2+U z!}rtHoWNK^1j9@h``%3zsfjn++p5NtEPZ1ag~$j3NdQKKE4zI{KPC(me}R)+*^Bz; z<=T&RH{ijMTg}zorn0l+TO){`7=c3k{YZ4{D0P^NBRska3JQ@L-rk~}(cQN9zi~Ut zA@uME51|<0;^w@<KiG8Z*R4Zw8cCvYo;AkJpblKLwLS?0$NGCHErJd<*b#b~v8(xp z^=33|o&9Bm^ZIfFLmpoZZDe1;$-d6WvZ{(kJ?eE?RN4~68t+jgVNby&VZh#l0fg~S zy(ARlf)R773i7La<2%q5!uoy>2WiUnVUx4~a}3L=Z~zl*Ub!S%IPirMuU4j}rgo}D z`dkahC6eJql=D&O^7%-t6W+1Wcy~!v9e>mAba8%pbJcj5UZ+a^#wk~;mE`Iy1L4wr z`-%*bz&Ge|@74@TFthW7`aLDo=Aj+r(_5Q|X||^9G<AC6Taxde#rHgx|C5$QN!P9W zwm!Ci+9vqusoQ59SBvYN*P@rw)1E1_uohugQIIq=4K0^Kd<O0%{Q!Zk2N<q_N!X%e z-Z(e8%9sSYO07(OyVX3&uqtxpC-5o=7{o!ZO8YXzckY#>3eibG{VnvYPEC5xgYcT2 zv|dLHG2_E>5$r&IJ(hTi)cArme%1#56swm6MM-I}by7I)zp!}aq$c!{#1%Jz?GC%< zc=`bL07Tbp{P^)B*aAECjoZFU;C+KTcQB%0?lu7tnWXM3D0Fvryri2uGJaxUWC(2# zlwsLWv(VEIhUwMR>fIkf+jxkOO<;_p*frk*Of-hWAub}4@`#O2e+0BGKv~i&kTXes z9TH^B_q{t>yBQx>Sjc+S$8VK4SPQGje|YUPe1bi396!_a5|V7DHp0CwFjb@}4u-9W z&NN{L@uf6)ty(}WYDO??fJqpsKLe8rNwfH?E?IH=Z_W?m3gfU!_qZi_va|T#UB#Hw zTt4-{iwTnxrm%Xa6$qezevQYhm|c9sq78BMX^q~537b5Qy-zC4y7_uHW*tzlmF?}& zVVWd|JR5X1hQhjg8Z_k*tg(2Om$MYgs9;EES=DG;8`zMnCYnio=ri#4go1LItee5f z;n8=uBfbi(q2YlpXVK2K(@j+quNGwooAyjdfA$g^-^G_4qU*<D`g`){Sac(MlQ;@2 z7*3|X$U$sm5o!*{e;EJav$C8>rcvIRrZ-fY*p=^E{KPQOCp+blc>Z6t>IUR#?^_vf zy|RrSMeDb^N{DLZXTML0{aSvSqYPb5uVwISI6{w1A`U0`RT;)^X0u&kcM>Z1A-F{7 zZL=~MOSM7tqRX$7o&DhT{%zjhi2|-XeapsUIFGn1M3U`_-Mkd>Q$3HUMVA|$i5@(b zO!C2{(AnD*df?NB%$?xFj&Men8$Z=q!)i*xb-4D5NYS*?|2Y<-<$ar^-E=8Mn-%HG z?R|0<Ga7x|;RhNm9ime`{rx_bKF+fMH_>}akvUI~)n~rSO@H(Q-z-oVeBu;0MzW=M z`C?3RXL+Z6)CVC^{KSo{EIYsBt|O|4fPddqy3<K}C`Fw;f9kG;J>8ney78FA!yi}5 zTIL1%OaldZADb||Ts}qm;EvsXb`MHlo_GY3_Da&#Iqq?D;c4@+UfSViQ+sKRW*e`J z0;@Yam&gqU>!=f+Nh`Y1p3oaLt$2&B&ON%hA{s2d)UbJlb^y&mgm+mo%th*riBacb zf68F#Bc4SYQu?qyH7i!<OZ-`kBWvUe{b0cwq;s3D^_F)I=x%J9J0+3f<BL^Fji){v zlhChPOPJsD`0dGw<2uF3foPeHGLNiiMtQd(PZX|P8^$DwN@ocnKq-0%0*TW9+;Iyf zuAg0=7p`1EIu}vPCTEyEE7HSnuwZ^DnBSpP6=vF@`{9Ki5<xOXh>93{JQ{^&i5j>e zmoxV;Y%`<%^{&Y|gX`yzxd?}x5{6IEeb~!-nQUzgJj8CM?`G+kbFQs}bMsy7$l{fS z5D7BpI@YItNm+;kP=)8H4I~QkqK3Z&hC2$~RA&!0cX9}8g`ohWe-~R32jEy*^(+>V z$HGOBFi!|_;g0fC+2%0Zz}*1oA3}pnm}6WqgN2ftrM6KzGz`job_9AUTFS2wdbPfj zkfV@T9p<u`+R9MYp8a^>RYH3roW{wqO{&?w{rcYi1^`+PQ>;ty#_zxTQaK&#bK2(l z>Va=h@6J*C-!sp{0!1LL@}4;&7_dj0PsWVy0u?{>(>M@=SLm9pt>=?o`xV5%O0`H` zw9?@)YuESh-!CR%BHp@x6drb&24306lx9YB1@L@~cw@|KdFutM3%73Hez{-$bQH6m zjk$^6^DA{2Pfq<qqWa-qV+7-A9|^`ibl#fZsT*M4fU(h<87lf=k9p+~u0qWae!kfj z0!UGmni?0U4xDZZ^b)r+oE1Nk{hp8n@8v9Dft{lVfX>|<j<3*7gtJ`UVNFCTXE07K z;-O;!VpRXn^A|4?q;acgr0&q5ce9C5wA3_R*iD!+<$ilN4Fj#Se33RUUuB{F7IjXn z0dTxet;%$Fi&fTtL@3eg(GNt7RJk@&e4DAwGw2XSZe=T`SDFe`8}%cQ_<kThpP!!x za-Wxr3-RoN{)3RE2)r=-nDG0GbAYpBkppQ8^Xj>61x4cjEIHPYNx?BlLO*NMc!yb) z=nm$s#bwx}kHJ}m6{F;K8YzXL^r|vB4Tgd*U9G&DoNcJUrtcq{es)Uc$|B{3VB!+J z-4`%^ecEZe;S0ksH`%;Z`-8Xirvv&n8#~Y;)>GfK?*lJg)T=>FqZzWrTs#c^o_Ft( z3zaJjnI9lRSLMshStuzVuA1wp)nmpO61_OyPUqd_iqy7AuqWeQzNY)|y12NwTi4^4 zHvsLj_YLM2Y;fZV!A0%w1KVU(M_(d}IPa6QwY63Ett|Ulr_S_u9NUtantCO;6W>)( z96Q=Sup%lA!W_J*W3n9$ubc_86f>=NiQO;xR(izGU+0qRM#h}T9bm5f9q=w>dP+(o zcYyN$qS}JVkYQ7MoK<?sXEH3meL{=#_xrdIBr>v|xWj^1Z!{FuHY;YnK7V&}=F0U{ z94yPfF<t(Dqx2OvRS$uc!ML7v@};kYl!E1|^|-HMQ?qR6L>!Y}=*@wUJc|?e<<aHr zHB#TV)@AD~QP=;=%gDE|8cTaEc0l@oObYWSuH(~A&l)e1^7YkKpezP|1{#>H|N39- zXA6R)orae7%hBWgWnl($hv7H=roY(!uNS;DeysxEs{GciTVURDcp`qM5x^jjUrkO| z^V3gFO+i5K4V&{xBbm_iQ{d%3x4WNxdR|v4z{a-NSup>C5M~5(D<5eaUcb)s?h`Ty z2{u&dL7xP<$H>u9(b3J6S?y`%CMCvvG0!LZ`NQ|CKX`n{`fq}b@rtJpEpP}RsTk71 zw7d=i5*Ukpg9MN<(2o40Gy#_F-m!y)?1%B08E8!d#T|!Rf{*Wlw)*58@<H}_o{}1= zsB}X%!b$9#@s}$3^Dh`K|L_bwVdVV<1HA5S&3`*zNQ0>ErS2Hn_RMAK^W&XvvQb_) zxgs!Z9WQ<XwrJ(Y)t5$IUS9ou5%DWj(PmXv-VYWVu%bSJf0T_i&^4WqkPuM6cRhWZ zv%q))8dl)uGLrp1*X!vaRKxXCh`_JyFPlvT?)^tcqL+9S&@lBPPV9^N1JqmaUtUJ| z`uV+{Cl(eGntydOIp`t41%6T@@{nMD8)u`kQeb`k#pRgQ%g-%%!78b7crPP8Zwj!W zE-ey#F>&t)ouABqfqAbL;cIPO5Vj0XQH4Jn#aA(WZ<tLQy+$0sv$@^7yS1k(C)JxN z!oyPr+L=A>u1|-J9yc@%i0l12SHJ-@hcT|ErUtn}%~WV$@76KLUlQ|zeB=ZZ`O-f} z*|6YE+jQM_apUxz%ptvh-vTHG5Bh%Ny&R$qp+8q*4fG?PL57|O_vs^aa2+utilZ^5 zov!12cm#fxp$BfnZc-~HB>dCN=viAMBX(}!F+rL#AnP+y{LVPzjZYtfiks+}m#KpB z&k5m);g1<YjM!Sb+Q;ZL{>N4%iHZ7QJX!TwDh^>Q5;1(_nrUQ4Q(r<lNeO_8s<E;e z;AH*buh620HTy4elOeFha-pKT4@7^WBT_la`L;S`6Fh}qLc%F~C$MR?)04c4d_+}( z-JzVW<f(3o`ui-l;K#>m0|;XadyF9GgkcFJ3dY3<-|N?}CteO@CTAR@B~PUg&bppA zAUb8AP?B}MMoI8||7uY*?wb$@-$cwYN&FP6N@>VLP-)4<GLD`k_1F<)+iobFl&Y8A z>=iGO?Bst?@wUJC@IpQUGl*wJXZri`c%^JZRgEHZsMYKi2Vo#WUPU)MNz#$+lKHxj zj>6<m?2Y@Mp7%L^#mnVfeU*+`>W|@iKk-496XrORMBzipb)nfj8y&wRE<ZOk0(pXy z`eRE*yCwUt^S2LRb=Paxl$`q1g}a{RtK~SRx8EYL{)DG+{wM!oUb^X&+dm?*dz4^$ z4Se<z6883}(ufENEg@PP0!iq;VPRsPA2zm!ha~tA_W|MlC@IM$<?-uENZ2fp54yT2 z1)<ouA0*tTJItMqK^qnSC?0H6c>@EQlwL|1S5T>U0p)`=^o0bK?haPPnU23;KyUK> zMw!AZ=#B9Q7}wP3rN?_ft;&FRA5=LQJcLf=!6D?T=v(Jm$zaoGmOTVenpq+EHxNjf zv=tZcpUup|at<vzChKMiih-!^FT~GCAVSVh14T$3E4%@&0k;kH!_mhfCQSQH=!i_n zkXN`0B&d$CQ29@Deg7~!2-aG8`m>JO9x!U?QY@Oh*z+;e$4`Nqr>r(scoA^}C@V@@ z+L&%{vF5|g&pM3Xh=$K~FWu?)gqw8J_ZXu7;8!4N8#bdW^=BAkDfIi{nrbf_9FJ$m zxe3f{lpig+vjAk@<c84;eaWl_eK3V<@FN<quh{hqB@x&(Jc_9xhn94X5S1Q(-S_9* zrTNHDY>NA<SMUP>n75mT_(n|^jtg@K0GwSpBa<)*xeXk~-wQ<SyFd!-J&ab0(A<If z&f}RG(d*x6UqJ5X69N(Z1?-tH3@ok=;wlO<$gAqr`At)8d038BlY@EO4tpC&lh(-9 zKo#VRWX7(#fL2`6!UQ(15JxENvS@sMM}alov;~p>po_?};1^bVf6tgYeE0kFYFs=# z6J~w%GJaTf+aJR1RAYTsOA5zO%mlg{Z|_U66d&WF<M;@B6n~4$$}$#e5|77WU-ow( zuHuaReGIA9M*oMpw+yRtUEhAkfI&%vbV!GENC?6pL>eR%knS#(76uIhlG4afN)VL> z5tWi|3_w(n5R?`P3HLSrYtHq4*<bd%kG=P?zpQl}Yt5;|c<$%A&+9yYC!CL02r1cz z4D!c7+^XW}dm9-;E0Gv1*0zPWPGG&A5oR_l7=@xwO-Wf2_#SRyF$0}Nqp<fFAA~~f zm;(7tCEyOWekpu$i#C8pL^`F0GiEW`CLuXS;IsN|@C;N$din<2MZcbNtl(<Rr2!pS z6GQEG-1hNXsHscZzh1dr2#a>clD(g!c92mMQ|t&L*%fc=poNuuP8L}Y6(LJ<FIlsC zA9xywENY3UP-iiv6%RtrDB7c(8sbvqvOwiI@nwsV6A2}kb{i5W(=$q+T|kvi;V%z# zY7?3uT?ua=N(!VQStO2)i&9fl51&XZ$dWLo7Lk&P3j<GSr!q%J9HY~3*2if$2Q$eW zMak)CQ~3VNuW(-=*~(TlCa8~6C?v)kBTjoHP1EHrI!HFhQ(L&#xs*+A%|S7D049pX z<>k(A4J)weJsy1*g&Hyfn}jceu9&)je%$+-w6--IK~v&&Yir0rmS78Y{VMfXo+Tky zGZ6|?e~YW00H3R&hz4zz(O*16fKAz^qWp~)@cK1w2TNx#@Q&CzK6?sLG1UZ`_sUmr z5E6cO$Y}Gk>E<jazr{6w3#yr17U!6@6|t1)`pD~cDlO*)1rsZyOwQQ$(@@w_pE_Ke zClj~0!kLzb%6;)vN6|Ld=`Y)YSi~VI%huyC6`Awr&p5wjH#EquZjWV*dyWYW%75Ks zTh25Q_9!9b^T#Ph3}27shg!|6LtLc-VLCQ9|HutvpZjsYBX<0i>DW@F1ZyZtnHr)5 zuvQ=h`gLx{oRbn-M(FmeT$|yFc#YlkCGH+lPw1Y|8`40y`o%gu59E)WjV!+$b5W_n zZb+z#ZGZDJC~y{gj&#KiEcDt#aIGQ2WQ+Qm8##T5AA6enLlJdp!8BhS46To%L`%fr z!mhAA>dE@;yW%4>4QBPA`?yH`J;{GhQHeS*3uga3i_e3`e<@Bcx%CDBjvPs(XgD!C z`1;7~G3PPSFtp$kVG&{Y1={u)+o4<u){G<$OC@NMmfwX3>|&0uxV}_(=e8{?_cK!T zO}ipFzelw15PXQ7yF=gfC)8r#QR#$zyKddV!2xa?=pjLl?6k$3GYTTm;h?F55R}R( z&j|pOr%A@(*^dnrDfs-5o*(V-<qk4`CsrONI8Dka6@4te?hqk2DutY!{H^>LrSvl1 zSY2A&E`q)qe-__YI#M!{5@F%Yrw=d)PcRnj;)Mx%oxMtI%vME<4Me)Sl2A@sKfe0l zBMM7kCSi~}0q=D_GH{M|hx6a<C%Oh!PL0gO$Ii%nOW5!U%WM1Czi=9DEJUPKjv(-- z1sx&WCF;e`?#PeY18hM$_#uPTtld=@X&>uj2!)o|rEbcsbz4MbmN2S`lvW&W*i^V9 zv1I~S^ShFW6c=Je2f_qgvD6_cNw(pIbS?BitGneo$~7T)u08VXAX&U2q$njvIbX@0 z8Kq*`lK7lcSUB_mR+?9pV8_oBYfDM1+0{}wh_j*A3kFztL+=cOaHtZ-tMCCKHgBX6 zADbtv{mXcAf@9|~6qgyFxh&zvvsc4<ebB~a<?PnylgewR-_!p+QKW%b3l1D{GJGPk zzhuR`3W#pR5Vqj{{UwwulNbRuk;?Z<kPdxSr!4ps?sq3=8RpLpm6%a=&$5KVV@i(3 zzf?zt&(bbzgiGg%24YpZ);~4h*fS2HDixl`DB?hZFWwVbZz($&5j;)H5EKEn4!)){ z!a~&o5dx+H*a?H1vHO7kK~N{j7}@t#CZ=pXLJ(BJuYx0xI*HLo0FY#pV2oQMnQE{? zuYj-FL;`WKdkqY9b4V(^2MUfAvyn}ng`NH!U5Exbb3}g1-=jBwyMko;NZ;a(q3_pQ zkNt=^{T_in7e>qj2UYWK$-%Y4O}w#El&K$ux<HcQuv!cu*6p<dT8cLa>_wK>@GSfK zA1YvVnT1GW^%mz`Snn0Pl_OpIwC>$wkrm~Uepi>X)jAs)K*%`4-yZX;P}1WY{3yD0 zi-nIqh%KR=*XAQ;U`2(&z4gFlSl(Hrn}&5U-{SbJGE`btc0q1Ktr;<9l;y-Nd|7@I zP06lkHU#)=TX&+i0XA2AvyB}|lB@@1SM@PH2xoN^Cp%UF-s>55SGX^6NtuOp79ue? zYwkTK%3W4g)?|EMXDaH!tgaZ^p2<~R!KFlk90Qny>;4dk*|G3K!PAl3>Z<x%V2RFB zVEA&CA-X?zgWkv2NGHY8x}|FF2A_OG)F6cyj|3a)ym{5g`TsasTcr{nGFLl_haRmr zGu6zmz{Nm(0I!<tlf=tFH8NK6GiRdoF$*|W@iQ!C(6Cu3%m#9Fw=lJirE5U$pKZYl zWc0HW>~W5ZxEBs<g)IC@bj;dWe5~opT^f!mxa)L?p;0x!J;NvZwX-w#5#F*uh{h0O z`@!-&Pq@V#cK|n=4u*Nk28Uh>wM$tVNeFXsZ^#Px5{iHU=n?8}8NDVRmh<|pTf2Xc zBK{i4S?k)%5m5jr$4!i`e^?nybuDzg@gu-wx~FS_G6DDVf(v8WuS)3#?XE4;Kw$>n zSY9Dwm;=u<g-MR-mNQ82BhWM=+thKcT>u-+B9WT{*QtA8FQ>s?BqMARZ~<exH2K2z z53pZ6q-q(SPHl<0_o>B?yAeHA&KVSEu_Zzl2s;7Tnlu0Hngq-emJ|_~ujVFx?iq%y zL;YgT2p9J;5d}NIFFqY8b8^m~`+E>CSV;du*n;3UfaY~&=TRZE>J(*%fBs~!Z&KqQ zWINl7r#!mt7_(5Bl;O7uqlE$XZdyBKOd*Q#L?1)Vf?~At`EqPKog<h?)Jdaya<Umg z+46nqd$hNiD*u353Iqm;b{;v{TL;Y(FjJf}7CtT2(D0bB9RhQ+d{#k$&dUTMsos(! ztXfdk1K!pZw!1WgB1gZc&hqvcXT9V7nPg;S<V$dknrvqlH(<LC-lY?wU{!RT&ZNB? zrJXlE&r~5r!eZVuvX+$LR2$mg=n=#xDoVkE2@+@MBIe|ZIiJ9Uu=wC#lj+JM_I(Xh zHm7Te4wY5|L`ff?+xS#*b1VBv&glmXT3f37&}6>T6cRpqE}<T4zCyM#cEwU?h0pmN zYOmjBlwUp~kx9n4Qqjh4dXnr+H<@^macYg@wf4jwRL@eQc$Ge+NpR-vRw-gRgg2wj zLsmj5^KWZA{GP!q?XR@cKKYAeZ3?Fp57S>J5luo`(xg4<4A*-5>ii?r{`J1b<?--+ zOJ9CX)cd^agIZTbJ*1bOQiui@WlPZ{Ak)aJ(Qc*jHcqQiZIs>SGj|Htn!Mj%7{0{( zGwf$V5@R9y+?kvT67sW6V*=bA!VKfq`R0KiMfm&=-qcl9#csH2`=-hw?5!1E`>9ze zE8vw+3wTL3zBsM(^z`a53A|1#HS*Co*MG`4_&1BQ+}B~FJNuu{ZtN8}zB&H$nd7W$ z+ubJx`YCtMOsh%pI@DJz!h^T?LC#)j^>|VHmDRmA)!v5A&+8jVACvC^nqMZreL>}@ z9whEt5QqP-5t0d9VvMY;LySFJ=YTC6QN6j?8vKh7Arf(8ld9tzOrpJA2Tc63N4In1 zxoVVzgmj3>s(lWB_h^8r<6>svTG&a=4{C;vLa10Q>}~%EPweinU)7Z>|6HwIsJ3`) zww=NVk}X<tW`l9D9xy6`1GV94aaT)@0KcT<4rpa$WG#QjgFhiA`9qW?B(}lZQD@P5 z!2;6XAfnQ}Vds|k1Xr7fZA%(uA*lcJ%+UY7k$!{c+W;Lzb*QL(mxq`PvLg88gCS_X zOwiy3l@4i#D78VzL5+Y*d#TIk5kzoc2i)`aZDYxo_(ySZanRHSDopcM)90^7E#9EP zi$0fl{hWeLNzvy^8<=`2&EMBnRafu+`jxp^*lapJKE6ZHO$4^g#y4+pF`IKWh4<dW zpKvEEtc7n@NX5%*9c(0Os;c2SPfV<Z|M~o83P5Kj6lQp8-#xmLp`boufNnMg`xuL) zqvoL7TpRb5do{?|mbPAIUzgnP6K%E~c*HN+2#<R%25zwGF7M1I$Md$XMN=w?+sOHp z0j^5fwjHxKoWp(>e1}f27euu6^t^^i$3Y|A1v3142YH&oKr^=k_;7V~hoV`=s&lXR zx!m6<16g^HKXOAzPC;J&Mf93caK%3sFMY0r;oyZPg2vSwFCh|;@nsTmk}2}@3*<c> zkFR~-wR|_$Gx{)3Mk%3C%w)q8abhU<X^B>T1EE*!?C97~UKme01_7Jl{{9y}b$hRB zU*aP#N=Z!~zC8-n`YQY-@4xdYIon0)^XIjdn2oHgh97%oQ*B9sJ5b3IlqbuUnyM;o zrB+gUQU%Gi+RQbuxc<H;whwx(E!>c^Y!R*1)k81XF%rMCPcmu1Q;W1beG{;rKPiKB zJtHP6==R6Awn*dk^`W>aPnkl5UfDZd#PHP#M6Vg|H2gZvt*u1`1^3`4+rZ46@aK1L zN@B#mPi5LwrQmhX5)&2r=mP2ChS-vC&0v@Q73??z1;~_-^@^2FAbM733`WDtZ{L<P z)UfI-E=+ujjC=X);OzrCLw{@Q2ZDLUAY6&e`ne=TS(Y8U->@%m^Ne241fwSdh7Xx7 zcla7y{)a05gUGRwo(Y><&Gq}0>C36C@)M;UwIg3DBF}!?i1>4F#Jxs2M?|UOc~MH) zIzWZ$HhSKRH3{SuX9a^q4y2>&3a=q>uP4v9^OoN~iz6^mYj7jP;ycRq{3Gkb*3dGM z!_p>Hlas+O?-wF*^8);rb#;@GD?gjMmAiHIyhI4-6`~?f^oE<?q>GX)O0DXhLw%R_ zEA1wc6Bic7CEP|tyR6DDGjbFccLzyzl}Z<`;f<NTKd7#xVHPdLg=f+FXGnBb8Qj7r zE3caVPC8-IhqlUq9qBz=7hBso=(3#I9uBx4_)oTGgUsIK=cndPC_3Zd?5Tk<o)sFi zKww{z^(ikalZa9c0@0ty5DyOzw%}P%($Y?r3#2-hFv$OKJFLTP=t>In<sJf3dd=E) zY0HC?{jc8_7sb&yXwz9>xgCS&pYANZq^|0E`!~q1L`6km=(L^1p?gL8^5M}D%P+(V zm^K++$Ap6ZG0LGw0$w#<(%X7aN-Aypifj>YEO}$g(AQVI3{E#vjB`Wy#*6R7&dEee zwTJMx7vI@4hh8I1S*L+4@YYHMLAhFf2!u#s%alR{SD@v#K=yOzHWwDu)UG|VT=jGa z3Hbwsk+nEPTN3@a=rKK5UDT!!a*sFOU$=qF{U+EPwq)jrpN~0`9qbH>6-9jJ6Wi_2 zRoMgA-c;Rp5snMOFM*slHf9+98kE`N)a(5^DOCMnP>!sGC@Q46O3(QX!v1RYx!37w ziueAzT4hB=);|vi(Rnd3sJ3nc&z}$s+eReOA|Tj^>QdNjgDq4kt|{|QWHHl3dka`9 zO3TYtEn420pwKY4L*$R<Zy;ztV$|XbJt<vfpt*FB@NKPxAlL@`lS*(*W5w*;+ytNP z^RIMElDA_z;6Pn{PA7DYDrlFFq5_47am{M^=Bloax~iULNK8-{-|}caMq(sSF}Us* zpETkGzIi)#3Z8dJB^jJl&G{&*1<;hAV7aPg;PZ`05e*g}!DD!L*v-#N(=H_bfZyPl zuX{)Ql)Up0qbEmh<zSW?{Y=z0SdX2#I@qI9sDOJl`Qw6zcr>viWgJwLTauNy)6kN^ zd{prW<Un7ensfM?S`|9lcRTS5-`)BEH0h(S@;C0BfOi1moA;J7lGzUd;-uBn1Afin z7iys-tLH`^oB&-OhRDM&5KoFi)o*jAgU48#lYMU?>=}!hrsqv(>hCdUe059biCA}5 zD~cx`q7-8n&Qx(@@}eew7rqshk}$3K*Y2%_x+IRNMUF-qrx}C5h*6cJOP-4$^o+Ca zvC{Au<SWIwz$c{8dp|#>{$=U|vlhjYXjb1WCE;%YDhk_BkP$tpED#^y(Pvs%k`RtN zgqO{Yy>?2kTvb~Nj>KKEsWfR}AwIvph$B#C4f{#PO!fgQrMY7NKzKfA>HhU8eA3S0 z@jWkR#sqQ<-Jb?e`|J;iuEY%9hBSq@qb^w9>%cyw%5~%Bozy_1B;!8*{qyn70J-K2 z?>kboT45<Fsgl%)RnC_~8JA5H&)cabDi|Cbj2!uU^7Zfi^C0(3VcHSK=c61-1Ii_~ zh;k1`BTi}G*scHm`4J*g?PnifUZ&hIR9*LFq*9y2dfkn(f7^sJ(>gghajZg({ec#< zGXWvCmi)T|*((G)GRIK(s9wwx%cbTP(_7&4TI+1>uqQak87L76xv22@h?_}S#hsPB zAD_uzqNicjuIQUAR_Bv&NUnGekM{6^cCGC`z@<2p2ji|{Q_3Y?3e;S_oE5i^)<_hx z)61osyLl0n;n2x3PMZ5I%z^Q?1XHwM(YX$1O20wEg;s=g!l?SUAXU7g(6`s%$w+Lb z)zU)AljO!@5(^~_pP!A|Hc?5|Dv=|aPRooEc;rzS=XdR{-Er2^X(^AYCWlJWrI9SS zFb+9HKk6nNe9b#b79%L}n%A<7bVXtHg7b@R%JP97x<9b1m5ZdU`SzX9FYa3e{+P)C zZ`4*O+>H}8T2H$um3cH<9wdC73q|CUGqU}yN0zHibdwb&1xu6rtmO14)c^OjjdX2g zp+5#87`gP(Iy9w&UDN)Z3t|IgDMMET^Rpi_jY2j4P^t{EJIORK!5P}0Fk_hv1;^?> z-jJFd&=b_~LzC|`d1An(+wAma8lzTDg|ILr+zp1BK*kxk2$K)vNgE2F+@=w5SaL8p z-`K-an3$TF0IM%Dy&yZkpd_JZBu|PTv;Yd)E_Eux+a0W1vcW|Nkulq8A!8tgIms}K zc+51o>5^eg9`D0YxYjLmH?|(>lw8XCeb4}7QmAWVcmOx2E48%zHR%r?+#?d0MM!TU z(Q22W6CF#kg3msm0$uW~A#q&*urypSd%^)RAbjVpFf&!Rvjm=rUZaY?h9GF;n;?FP zXBV~rEEs1B{AOyAhAe7|@8GknEoM4PA)#ZBmiVKYP?$B;O-!l#mZz1S6hIKd`l)uD z)d4&21P5O)ZCautUNxM>bBIE9&2v$VYpjYnGswY00HoZz2}~AulX#xNMkihttO|c- z>U?S`*)C=Y#z>Hf#=Obv9AEA6Jm!|u)Fed%@vzu>nTLz_3-$_*b;r!bsu6QP@7Pd* z&1D1q@%{U4TH|R7O3I2OUojVCz{De9xLzb*u%zt*b-;fhAgOj1(I8dkq}+U^7R9qy z@PgbljVx%1N{W0HPd?FAuqc)&S5k}dxy>7hLpW8DaNV>RcLK9m_Nfzdm(x7B5MEVJ z=K{wD?zYits*dIfwAjjg@?{ekhu=!YP(9QvSvmU(|MUNv%}CJEFXsAS5xIKHmsU>B zf01|>U$$L3YOr0Dn=4@p&c%WAJwn%?OJXIXo>?>wluf~&uTzvG=@+Le(lu@r_7)ik z2ZRhlB(TE@k*|{9R!24mZ)_LLIT=sJ7!~Wj*XEeGqlebQF0Qx=WcX)72vks5NW!%_ z_GjegGOkR&h-2jG%owOTfBrV!EfbTh>b34<!17OOXcKZRC5p1{Oz4PBH~$DHEI^|< z=vv_)vIw~-pyn3T)!(wrsfs^_&X_HzU_I}ooE!v}TK8@l_X=(lmqbuU6af$ZDTXoM zK(dwciT8|0)MedpTfaFoU)E2=2!8aW03xB7%mLoevIsr0wAzQY<GsD{iPzu~fw%@E zK%Y0gkit1GFbQ@_0l}VzN^#j+pp!C<OTjHGsTbyJp^2!d=(mhl(P&I{Rs6j8g9N8# zwTSTmRpL#FeOjHxfbzU~Fz+>nT)4DhY2n4rbqjzb&g~840d2QG#tV5~6{J97wYG4r zbC3>U(t+27oI*Fr5YiRf9gPlicdz>8ebG<!i7{j;^17(>hqwf?gQO55ageylS$gj; zyZ(Dv9}`gSh%+(9$TnzC6}-P7o%W#9<k4ruF5o7gzC`HR&=&hgq1#0fClJ-7IvmQH zvvp^{EZ-_AYe<h%hA4wS%~d=$&gS-~Hu%yKKCqd@Jg|lK$1aoQS3A;pe4r=s|9dH4 z87WdocIP0#XJjR55${C0ldbz2Oy0bG^(t$uVwHzG9~d<m7>KqJFUjuOc7Ry(@LUFR zdgM1t_OR(%%z4Il(5|gXUxf)}lw^u#I=HjmSdWnAL7AcF$=2JMY>}K=oZ>{$9=5g| zRQ-=fsb2bgRZ4b5ln3lc`s2TZMIauEQ$*LvgpYTTXBa7rz{13UamrPn<ft+#xcr#Z zJ<<y2tmH&eEzh5yxnS2B10me}HoGpufvgk5qUuUpe3=VgjsYqpX1x8555*lv*=KJb zANZM1lg+DaVO_2T$Y@@IDC}N=X;qyt<8?$)4MQ7V0w!eaycam$5{U2u*3v#rNP7V_ zS}tWdTH7<aV)rb0H|4u<PX71|30-IEa+1;tk|4~N?&}EY^|vz1=YcXIO>Z!<nH3$p zjJz^|Fm<fhf{pK_sP3c47U~+AC1X5G9fxg^7L9$oUyuldcdl;a$VYD~LQJD>;p158 z9tT#aIy4W0IZI_KSNI_Q((xOh2G3r;44Qt02v3|?CGjuu41dR3C35oySL*qg^ltXF z_{v>jHhuz03ur{jzf_8>BO%nJ7N{tAoMTUN^#`8&u!X<CO~5*|3-u=mbv}^Rjdv9J z4dtoAp|vPtAv;|L^rNX{@;E_}BXXXg3~dR_&9p?SSaE6s-AXp8WV@JF#<&}E`pN0M ztUOrS)ms4>G?bd!x_Fg&8Z@?M2}MTSwGG|$`f=lt&br@t;3~}3E~k|!J7(jYy324= zp7!t^Zq~ZXmA&|aU<dDOZnwU0#r)6;`Wy{y%x?K2uL<t>bn{FGV3!BM$)Ij3EfM9I z_1Yo|AjGoiYSIJ#zNRk!GAKxvc7~Xgm*~hhuKe_Bw6qx9Ec1_e{d8Jd&qt=_Yq4zr zP2T?S4HxC#5np#G&nXSHi-{Q^nht_(erc|@xr9BJ8Myl;^gU_*U>JTm61jOaA?<!^ zX5rNP42_(KUn?=$?H5lzGLOD|ep0i>OD~#jcva}Dl16Gv#jQjEhSc{n&vSkIRV63G z5czHolF0WjRO&s~>GIqU_iXsO@~y7-Ff({{I|lXrfyL1s=8f!@HmlkTRyV&kvl<C& zid@Pq1Wo2K_JV244X0@YdH&1&tMyKj)G3W0*&2nH$zTbRo{-U8@=o@8W0lmu_=4b9 z_V)Jq-l8%xU=zTr+Yl4}m4@SOdq5$pHdIm0zi19rQG_sjYF|>Bf+b*LGva@+vyI;e zIA})M1<FCf)==1k_~Yji%SJE;Q}x^USY&2QQJgt5@b~v%Q^9+Hh=Ap!|7?)}IWcis z!G6_<;D3$)i>2Rqfi$$V5pv<jz~y-3)R2>t6HQuIcPv{HAVz!VJJ?ZS5pw0nyp(7G z@J?bTyH14r4u5?o5g*b7#bPg&#EUL+pnA?os3r(l)>H-_-eq0Gu&z%^P3;{?PA1!b z^W^cx)c*`S|59lFZ_pP0KS4bFKmC`!kAh6Lk+CrgGc$V=C*+TR2mKc^VKD|XQMRa+ z#f=+B|AW1#>b>!=Z^p#Q)YQ<B>SMLc(%sb5uT5SzeoneLI(mL?r6asw42`U(r>C$` za+7EH_yO|!IL?2+3?*DJU~*@)v9>;~NpUw)Q&aQy_J$AIojZ4g>fgP85C3QSlM9Iu z2L<;ET!Ovoan-PE4+Cdu0Z#!*gSyt%*8ACt)&oOBLn9;MQ`Z`dIdnooLR9SR;&X<k zROA4TQrV?*E%?blGwuk-!RGSw-#S*GA*u!zu!(Yi#5Yj&74`uiC?qQSY=fQbBe<_% zmw(W;mM7s5XL2S9*xe`AbatQM?*B2Q3yvJ|lHaR0#Q4#&GBR<ovB3$9TZ>>dnaLs` z0@WkaxaGGw#8^TPw&o!y{3_%=v7G~iQwRtMoRwd-IegcbV)H*UdT@BCCAvv_5+%gm zM7MrDM5E`9%hPpQ2Gm$DE$@>yqyNgLzD(@w6pFuBC%?m9y++^N{Rapj79?YHm9v(L z5vOgWX!uzW5)wAG`$7WeCLYerS~dYI6xyI=8Gb6kePhrQXHa8HOH06^@SP@kf1&Ah zLTH~836ZxaJr?jo;D%!_<#IiM+0JXg2mtYTW4Ys!1{I~Hvkcgq{{Ar;aCrf}rq#KE z1zo^0am(Wf@dtVGLEA2@z?oTJqGSb)@!q3<eFbRN@{~wvULo`o3$cIS><J#Ui1D#8 z#Hp$kZ0>Hst*CElk=-?}Sbrmi4cwW9g@u;aZF~^8;zi(d*mJ9tuxVPJ9zc_xAd{9_ z3fvfuDv6^tDaSUyz9}xW-QE8hL3yR~l|H82(K8^Qr$Jj1bK|k*koXlX%4HyS?0|zY zo7~_+lWx>s#^j}x6h^fWw|lH2f`SqT<jfdv$(Gk&NX8o8$#E(u6twa%{Jm~!+OLc4 z@GfbHYW=ZhEv~?_u6k<hb0?G8spMf~_gVXLgri-b->IXv@gENipN)nFVbLdB{+s!B zJP47~zp23{?xdN_j$JYne=eXuzLSAZHIL|vfOR;<jA9WDML*vBd|UbU&S48G(L_6Y zH<^rNq&(cs^HzXyCecRIzVa0hk0MrGyw12h&Sat5AKiZU1`M=EM@RYfPM)75EEA|- zTeSA(k0eF^?R5+I$y3&yQUCtJn^SXBtIucMQc`yBZRcKX6#11&e@#C8E_rz>^X6W9 zLs{iV&+nQ!e#Bo}6@R%SEQZpG&Klm?=l^t@&vHGeW+SdpaDW$2uOCaP@Fc>jF46t| zz&{6ZtCXhiFhZKevJbHW83aGN8$iYK^{sKjr)5Nq0ABmE?HTDquxp_8L8e6PdOUvx zY7XPMbIQ<zjODy=;Wi{|!HNilB+x%X<GTmxFT>B!JPCzq(Cqv1$#t<n4BrWSm<3B4 z8g9Xv&WNV6Ct3F6b=rmO#t8IO)Zc`PzaAdL+aT`2Q4cY6i?$$%kM<m7M&GI0q1v?$ zL~dL5>yGm@*f6L>hL=y2t-rtj@ySW8ILKPk(zp>-dM|#V*N=c)SWFDS8#4E!b~+um zxIQ(+CS!kr3+teG4TNBBq$x(-S8=$PafTNke>gG|wHZK2sOn>)eyH{LRQ3q+>QR(_ zd>_7{g=^Pd9<`7jxZ2Mi1f4N3FmNdt>C_3rbT4*(XH2|wTpGZS<DJ>E>qg^CL^Er3 z4aMgrZz~hnpFf(Mm>6+#&WPP8M{{gKY7^d{Sq77Jb#=l-Xtl?BPpl&TB)*nH=+PV5 zOD&trplw=wBjYBY;#oIjU7A|3ysamf7=gZ+F^>9Ch7!ghVCZLqApfnnw=iA*P2l_^ zBIwT>7LL8-nZ{_@#Sd079_*PR=bg0RQ^gC~ZQIJ9V(!X0Lb+wbXZh=;S9jU;`AUKa zn~jx1SE$!2%Kj<^XGk-yp<MUon7$#o^G>a5q5DB7X`!}w&;C4ted#R;!Byg@d8n=m z8k*)d(|mGRjG(H&>TPY^>Sd~8$2skngOYj*coxT|yyJybDC=g_dR(k{`)(Vf7Q{2t zqra0}HYQ+~+v}oS>y@z>W?z#p$cQQ?p$Ump1IT0cNi+5&Ah0HL$Bn^UAp265!Huca z!t;ibpRWWAq?<Y2kh~lG^tF2B#s6{}{)v#wcroAd2ZACJpRndTR3R#uA-r^f93nC0 zcns7u#w#K$I}d`FnausRL0XeUsAbMwu<S(R=0iD*1iRc3=yeZ?o@Kx$)-`~n7j!!| zr_^biJHPLbB?=DsgX9IPX{Lf{My0S8i9~8}(?81{mg$Ez2|>Cd^i*ZP>b8f5P*2aP z8cq(5_IE7xp|ZR+wGj8o*J5#_RuAo=*FOMb7KTOi8vy3nu?YM`Uc<AQNRF4QO7^C7 zPS$mo%j`)I9a`#7oREZX2R<v+tbJ`-!r=C#h77u0SDAILl$Msq#l+Oa({ytd#u@P$ zcE%k$VY+Q;U(@wDL|ZThAzZ`ldG_Qs`|!r7&a+_7&N)p#j`89{X5m*%Tv(iG3zlkw zSdw7Uc%*7owgsvlg_Ym{kj}2$Fv-}2nVeRWyt-AEZomP_0_SZ-4N;dXwsh<c{3Nd2 zNbX-mF_{vn2c@zP9Yf+L+V!d*vE~zs-q;)POqe`K$#tYamrCT=^1m*#KCQ?r%~_3? z{;~5sCpSswp*5dL&-xBSZhJf0V%XC#tdEl)or$5p@Ojn8)pl8{!GS+ff_+e)N7jR2 z*Ntr(am$`?y=)=5cQMQHHPUOaSWnZyr0>LSpo1{%G6RC%OqF3w`VgP^K6?YhNhaH; zEq;FP&zz|QEQa)U%2rxdi*H33)pSyB2;8Ly3!s+$H{@vaDot^2%r7mztHW!ZFUArw z1gRDT&F$&xot9VYFxF*txPgF=U}c0m5VQCDw^KCrE0{6J!rvTy-bC|ibc5zAZj_&_ zj|m5pfTIgc0E^|-$Q8S;xV-pL^i3^|{&hlE30$x{3m_sgd>vYt#@UbH`#)Kmu7{*u zHl*{Z-}`@^c}GfwIZZ>J{Q~EntL~Q_<R!$c$vA|B6Ns8};crP2dyOOGb!Z3qLA1A~ zXjY$_H-L|yKbpgqm5R?1#P;sUHF;D=-=)il8S!VFERvlXWc-YTlQ-TFx+aX(`4HiZ z5n{W<OoFrvT1|i3BkKhPv+uYR#YvNnIi@cOO^xc;qY3&Hg0`?Eh&T!SG3htvm~yms z{n{G`LlaVjNVDxy4><mAZXukQ)w4C`g{F%B*3J@O9~w==+@2B4Jjp+GmLF5q8FrAO z`S{6mnC@8k#<?D6J?=s58FTY>bZ~I6wn6M`m4N|67~J0{9X-dBlJu!xz?;8w8ovPE z_<G#PPiDNinom~Q{%D3>p4?);$9rvBjlqFO!3x1_#~*8pAFN_fHBuLQV}*@E{_7um zL_SF_(pBZgH`-A{rQk$^?^Y4{e3=FpV-bW&t5@(0W(O1JfXT5WJ+F=xXcZJzJid3* zi==%&vK^J-#IDodef}BujMPXGH!rV_K$VHczk@0y_(=!PpzF8a85_{eJbxLb>=;HT zkDO6dU!lMmi4Y8OS-H7#?(YP&KEQS$8{A&1{M`?a*jBzj2-VM~m7b~=j5Q=>6?^*( zFbcI_5z^?kfOU(W&=*&JzJsC=FtmyW3RDvJVIBcK&A29d54^BNQjZ?iQXl2%n8{%D zi!RbNWjd8_k?Y0vcL#0LncIpXFZil^->?Y$C>wn?&c?5Vl>m#JjtfOd^O;j%%6&QI ze{OV=%yV8Gd@<GIkJwc?>Urv<jNsSgk79oNiF}Wlovehjf!cOwf8U25TTAsLqe^Q5 z<vQex*RWfJkhY6cb|R^}X;nAd;$-0IDIB(nH&*UjO=B51%4qJVs;QX}^9&))ODw$n z{rjZN!2tT92|qix4x|p22*=iPH0+|0t_!da5YA%k(u579jN@%8%ndUWM-X)Ko|)~{ zIoDA=UgRCJy;hETw}dygYn5J9q_*2QyMO;}cNWL~_)=DNBSXX_cSBAwcm*U8Ue+3d zzmS6u*yZkce?WsLFo96pB|TOxm;a_4#dvCNHwka^uWc45nn=DXEA4EzDulE?cW&82 zjmp;t&;0*&s0DZ1Ql?6(-q?GBfUwggSnO!p!vG8u|BJ9g$!;keTmup2r}SPLh7lNK zoLpRpP^k7wy`RPr$F(<_z0+1eox~~1W5_d+_no8(1_k&ht^p7$R=sQjtUcjiZbHx} zLea<D5eRwG^64Vh7~cZnr3Ubr!c5BH$FN4bYHdy7g)7EN-aq}+%#z7C^EGH8C6<)I z{yQvsnZ_j+3?lx}zLlK76;t^@MG90%yaewTzoqJs?+Yi&bsY3WVc-CjDNwA1x7tBa z^`+V8>r|}+K{A?a&seDt$>sB@Nsk?mJEi*Q?6Lzt+A?6FXY~I4G?cPDYV>~gedL(G z5VHc3BLn%t1T}yW93#0FdA}j6sh-HOG}V@RCFV88S=(zS+IDWH+=WO-r?O}R(L2x1 zKQ4Luq2pA-`I<cGtmU@YU{R?|`^qfG9dC!a?zeAiiNfN0wnSdVrQf3WPEVwD)1M~w z%6l^Qn>_FwLnbdW`0#KFeKrz&x6@jw-;sCRJ3POK>bOQ#*DAqlekb7aqkH!p@b@H1 zU8bX#-VqCac=v&Z2E(Rdq+}J>Q5wIzO{uYCoW0;6^Oy9Z1K}iR_|6-r5_Sr~wxf%V zAyk%epS3W52RB;TY|?3-(@%4eXH&gb2;7kN8MURfYd3Fyp_VgG^<r38O*r(THcKLX z-lFtVSiNT)PLPEDt_fQE@wt(Fm+yD^6c@RdfsO&cW-h1Wr(Y889r!4A_5}gGlArS0 z1|(;0`Xm4NH?vFSir7-7NP1x11ih#01I4Igwai3Qr;L0iXI30~7bEotIJwygcLj?> zoMz%CeJ@aJniLRcWvtqtd8h#|71Prlj=}xmwyR|{|K86l7`>n5AF*oueDlGM#~`G! z&S*L<(7uJ$$)N1yyFWJ?c^c<1YeR|<Dpb6!?X3lI@9Wow^Zm&T6gu)xMz|K=Q*HRn zA1rMoQ6w+&vi}wNz7eqSIncYIeEq3OiB-{LU+Al>;Wk%QZ)9uk_*~HKt~*KoRU<&V z!n@J81O_sqKW}_mTjDD;KXi)dnvqv$DLL(m)O`$ZG1*UgO0VnHVsox>Q^vhW<hwzz z@$jo5-`e`sBF1}bdS17VJ>?sC^et$EMNc-^2}L(Q_$Ibz>wE|ut#ba?Z^N#DY(d<r z-*os=cl$}T9mjB%pfUULlgK}Un^%m<nkU9BJ4Sf;6>Kg(1m3LPKR=VIUVSXCUu~(# zTQnYKU)c67UJk7b`hz5hdRZ-X?a~>L9soD@w?CXx_r{yK$MEmo{a3B2BDf&JQOl61 z<f-;Qkk0=d-R8xUhY#oBk~wnS?A^zYaA_XsWx&xo%iN!wl8)WK57!KTyjxHnhS%^z z0;d340jU&3(=0KTB=is5qGS4;vN4XeS@l#BM;+uYu9Y5ma|;Iq&4vGXU!8DwWySUT z1BfK{HGlZ$k0wL^+WfHo39wR$P2g0ns(J$G#MC_a#)ki)ak2@3lJGhH+0&=hRo`s% zCY!Azu41lTat7urPt~($fM0!?TOjl^7gA2-R4y(qpo;+}xOjA+)iWaHTLpNrt=n+i zz>>B8(NTlb?JIEe1~Ox#fhc1A)BwIXIdHvYy252g8H&=~+ytqHG=N$J;tFtN9UUFr z%%}4f7vV;g2E<$-H4peBZh_gy*_n?8f<1=+lgBeX`j`4fXli;Lw<fA9CMfNNcuXK6 zVcqZpXbps%AO09s6D^j7nFRon5F?wq33|Z4U(COHhM}qHeFzXxNnNsr=3qufGaH*x z17CSr+5O&n7a-;z=HV>HD-dD>ok(c!2ha}5kgnQkZMff*QRBb52>(@GUET2T({+VF zQP|dBy;=)*AgCM!Qx>pazX0b;Z4SuY%?5>XwJ^8O%*?=VB_=i&IpE@z&MP4yAt{-0 z$rFqVN^69;U+E9O|8Ch$jEyrhGjA>p(D~*SBmAtbt-Y;*6w`cQe*Jn*_JdP^CV^}L z(u<-S?wiDvaylDjZ2x%w1Qd&`uF<H@&iKz9xG(mS{{Izv>%T__?%(`>4H^6Y)4$X` zQ(H#|sB;&q8sJ5KKPyXGMkc@{l2SBTP*n63C`(H&rl#+~HGtDGpP72ShutCse{bZq zw6^AZ-zg1wSn`ffS#f|1FHvnqRd(FmP;*cap0D-r$FE;MXlvdmx?Z;P_@BeawBYQy zbAKU_7--@KI19MRS;K>Kh3;SuPg2Hnii)1TXAhK7cw_+X&CSKd-el!vY;XS!93`NA zSK@i?0>u@)cb_Qljc55j_YV)%@OgEP-4EiX1cZdZ4NE%M-QTx?MeQpi069LZUkXp4 z1z*1x^s;&slY?<>eekhS;(xxHm5~y#C1qu0or^f8HnGq0vf%`D?+=JX5?TBP;Mg^Q z`r1ZPLS{C=YqljlWo&E=5SE}qHWF9|OUDkwgyf+Ia;a{OFi;OcgaI&K=+;pqoCa{h zdF@dMc;l4R_MK0qAC4Y={+67Ol(bN2UvTX&_^k~?>xU!I>tEkQfH0HIR*bWESZD&M zOBS+j&XnnaApNgDF47tY6xht2&xVya&teYl^br8y8bm{Y0SM7uuvQdFyBQsA94WaY ztV~R9!52l*&<m3-D3JL0_zF$FsZ|Gm>-WDzm|9K${rfdTGB0mPA$Zi0@2#JGGG7mk zPBL7lA<QIeB7N;F>G?AW_9l0uq*c^a45_!swXa`A$8yYWPS(HP7|qk69@HChdKJFq zw;><;8~|ZITZ)frHeY13is9dbBR?NB-mZ|{5t^ikpJD(Mjey-j$QhGs*Dg+12FO8$ zC6kkZ!D!cqk`o4aKY<0pgIv)3O*lS=4<A-2{ck9KzA>h!grDNVT#D@OMFbneIWf^! zk~qGq!tmL`{^7x!sce!}UvW2(X5fu*kAr?00SBY}GoUJhrUFS|pxddc;vw?03d80* z2@XjW&dKKiW8OP-rn?N3Z8+g=+Jdek{=mEn1oBz{4KtjD&czrVkXq12@IL{Ma9T|` zDjvYZIKU!PMZ6h8dg#jSXGY1M-PLOgr6LsVjesm&b`#`w>@j6k0;n*MS`efo_yG+C z>3LeTS;B&Xc9re5YX{wG&-@?YD*<g^uKHvae3(w0kv==NkoulNFuyZ{RNPg`FGe^! zF6v4PQmMpE;>g3vNn3khcy||GAW0-7AoI#GLA>5;+#^9;R8(K-?pB(TWXYJIrIVlP ztoXfH*it%}V~Lc(V%mMh?=+6L4f58CkKL=UlEo*I++pzLxivM~*Ox}!?HLw^TUOr@ zdJFqufV1fzsy?C1PpfW^`0<VL67OjtXQy<pq%a76c3O!h{N#+pqVvk+c6l0N>55BB ztdrb72vI0;T~sg#@Mhdv35<<>0!PdKJw;CJ5~ZU3@}T^%hVLFoAp2k6DH!K8@J+Rt z8fv?pbX<B>ONLPY^jsJ1MS+EtAc`@4zjmQkwHSG4;slbv-{`;*R+q%*dcEn$Ayd*> zu23D-!d=60?)p=UMnH|mH>S1IwrY*7A$xBmJ5bffq+jz*MV3V~b|61A*O^DjjS28N zC7RJ<w~30~RL^XsyV|;dS1tg<M-1hT9ZYM+f#6x>;q2#56lwG&iCEMB#^{_#Lno-F zWT>&BbRMq!HM{ng)ond`)XS<&|M_S+P~DU-JdfaHmay*z^CaURC>h%TPc_Jm;6#YG z&3yq()}CAJ)YP?b1L=~rodzn9iu4&$G~nq)d3aEZKVb1Y{`qBrtPddKl?B?>N$;Hb z!4xUHa~wKSC=c12Jsmm1=}9g|qDz=uc;@ZO;~Tcf6<T(|Gc=i<6<OQPJQLO6ihH1U zy&+alh3^6<q%VBTL2y+%dSvQX3)2f$F*tl}O5z%I#?}FulRZ^EM;~Mdu&if>pgY$N zAD1Sv{TjA<H%@?wl~tDqV7!S07{Wg6P`O@#Z4zu+AXUGvVZm&2(Xo%pknq5Dx{mj@ ztQnpqx_Ec@mIN{9?H}(16%`BRaHPYzIT1A};M6k`d{3nN0vP*5WRyRS4=V(j;6K0` zmKb6Dy)xdV_@+YCEAl~MWZvtOy&H3cWItwKNG<pS#}G$$E(NdU7njg#igy!SoMmT_ zZ=5Qj2GW*QWzG@Kiw?PjUyxo6db=WpD!;1nBhx@>o(9RUmcSpadHWj^8?BVrjI<_> z$`aX|niPLbstYXp`*<*yT+r0ULVPs&&T&USuulV5Ct|1Swrs~#jJEthumw}YoXuT{ ztOG3pM!bv|MV^crVJm0M(67yWzXZ96aDbTA$oWmxIgzT3LEVgGd>o_YR!*418u-Kr zo-dI2CgPh^2j3(?Pj3Ip?zzX7)J*jYQq=Q0%=TvH;<-EUSMooQGZ@p{QHdr@S5$X^ z%BAWLW)J4?FLKJN!b0z-sFD$u54|8B$yF(UHdv(|=a#c<#Fr)+VpHcDH~%rgmL1GP zS<)q!kqyfwY;<X)d8!usgiSSAC<_%A$(qj>@vh@uWsLLue@Wi`j<~07YW&}iYsnDV znKQXkkhQUZxUwShXS)W4hjr@RyHQS&QUZH$Sy1IMdT_|NGLLy-gZKj;hM|qK_=dTG zc^Kqb;qLD4sdLkJ(S_ipjpWA8ga2<WC-*)lxH?!dbQlXm7UAe?kvvGhP$sBGkj&OO zSl-+e<LO|ftdDwF7_->!!HO@j?m<XA_iI!+z6zF(p}&97F)|Y0-liTvuCjh$-;Ox_ zRZPKxj#kROK#yT>$@}1zK&9f=k-d_BY|d)DMU{z?Bj{GUniULXG(yEnvA2*E?vc^2 zo4m-XZy;PNb#Cr58?&>s7e=nBxCD(K51-il{llRD?F(d?7axYh2XKw$Jcq5_1>Y*z zZmOp~$$H4sV@^4TxP)h_NB2wijw+II{0d$In;c12cZ6#OUbQIWwIEWD<n%izWl)!V zC+$RNhzZ^ca)(E`ngM{5dl29F%coBTtX<@5xmW6WT)^2fcYP=R7BA)S2U3fQGngc0 zVID>~E{zXXExyQE9*nSNf>8k!LvVA?hfnKqRxuH&Lj0kfcjCdRKSmIdkAFZ1{<-Hi z%8Ep0=GWWrQStk6N2+*TA&1XYj<8NQ6NWDmY1sCN>u@MiaP+A(9H{>2ZzQtuadG|+ zoCwDq%y8fV0ooW`?3r~^kY1lv4k;aZ0@H-N0qxnj*K_60Y~O0=oAqzV4QU8!v3KeV zJbp_P`zae^TM84IS>{egeBGMeu<qE_cL*(D>yte*`Sj#QKli#nbVi$0wZg^3Uq}!w zOCzx_b!9-Jo*PB$yT)jO<Rzt);4M~;c%YT<c3(c@kWN&>V8`}3Ohdkig)?X=HGE{} zr0p4(56`R=JU$332r))h4Q;^76wp@Ib&iQAG{y=INOlQ3sS{%j1oKx87W}bt9K1`u zzXNg~98#EmJEU~CFkvKq7={`MADg?f`w(eS#fP277)zR-)tP8L_!aw`MO6NUbp-2e zeCb?b9i-MWw}!3UnN$|p5j&MFXoF+@@KSq@KfW<=sVMLZ=Um$U_Cha-@Y4Dw5>@Tw zF$^dRGW6zjeXre}SCr@u<ksag5fHFMh;G!T&6OY;KIgMx-O{5Wfp1J$Y~DI%%zB`# zQ?C6KaRS!QTs&db1j=p4?w@$BS{S_oncDMGh+(Y|^|V!H=i!w_0HG@e(^z+_sr}sw z2@6vcL=mY0?!t?Zp}mZ5I+KcpMMrz6P(E|qKv#EmVg1sbKf7Z!;zxTw^ZgLkZ<#BP zV7Vvv0AHH>a)@m)XG?l^6MT@t{Rub3a9A1x8zocW(FY&&ht}BW4c~K@)zuBtkNT7} zgz5G+KI?=~8Mvpf+h>RN4h|;a)3Ee7z37`r@=Pxb-kOV~MQ473S^I$?2U@tiuPX#G z!$YaV?f;-rV4qE+<@Z{IC+FIZGNEao+A%eT&#<MJIqsU{dWF#i!+$3&>OQ>q{x61% zq<mDG1ozhvA}(-W6Cmkkq(?5G!hmB-fa_-=q{o-g%a^7OAL#F&&rlrd;$yY7P!3pQ zM38TY_kKjVmIvUE4gG}sF>2X$1=dV2k*>ysm>#(gi<>(;4J<M6hf2zH;DrE(a{Vk0 z0Ro-R;}<ND^k9^fOJpNX&4e8$M{J2VrY8?<nSgHc*Y$<D1_x<h*>;$EhuDSJA8S+2 z37GFvIh04*efLrZcl&@4^eK%O_^pSeuOT3mEpo%GHbKxH@0=7DVXy~YgQC!V5mr1b zAuA=T9O4Pg)#IcJ`|qHLNFh*$puMB&hx;t%&~aZ;&GYLcd~}iuJ!M^v8iJYC685<; zZXP<HW~msMC(hHAZ;h=HQ<9OjWn^`CK!A@?d*^N$ffEmhv#(#WZuy_Tr<;F`G-&F; zwK2z5`KiuH$A3cU+l2+hB`IBB#rfD=-b|GM?Ydl4g!~k^DDbbG*06K9K;FP(mXDaM zwT)!t!6$vhE66racOt{2!dMYw&L<c~0$mlFZexUnH(C;97N|(%N-@*yW03TIR?p21 zRkch{aqYs+ZayAQ7aZ)1C2OvAy^;4hbq(yNGUGk@gUuolyM>XR!@inROR4(R?kmJZ zcw@qf2gT}fjmuy2O{zlvJs8+Kz_+@#4ND%H`#w9!8+DaAiL}1y@x7n|YzgqLJ8C>& zRScq1j8dLcM5AWnWOCOk1yUo^hq@E&ZNj!=D@PVz^-taeVc5)8NhaIw?6S`muZRe_ z0uqTFn$`UT(^8y=tmhOWZ(hcUgG3OYSSe@=fq4KONsLG-iEW3Q@B+jZxKW?K{fDch zE9967I-H1rQXPjkUk@Rwg)jA+c&C4ao=!^l5&Tto74WAY7*&<41eUzK9fE1NP-{!? zmBWd7FwtQJ4zIC};QyHuJd4*#Epc>xKvGJ|Whl1;d+h(?>~<;X7zuSxC&-BegT!yy zeS9WWe0+QaM7X)RR~EGl6_k{|m^rL%75>H-tu^GlB|S_96-(%tZbv|wB-foVJvGy5 zh`G>%CzDYvPJsUMIdqT8MalkdYJfi;mIP1htz_c0jLgi+k@i~FC~8Pt6LQyir)QP8 zO<Sdy8}+dhJ?fpC7&G<|8o5`bka|v8f0mf^PEKLug;j=yMpzI{>l)||g&@Lig<SWu z^22ian>HOx+Texr$>o;5l^~<x+9{6AzRy{ynt;7QLy1{#oBn}Wp%5YpzCpUAb#)@{ zH@-uB=B>N$F4(xMXO${^YVzTKu$v!Bo>dX}j_VSmLZ{Bom|r%kL5Epfph2g5MJHSZ zNiS!Rup;atV1SHoL~&V_1ui~3(TVK7E5Q}&)Kbg3t0>7hPbSOuS$fGYSFu|%xG%-P z#!Mx>zpg7B^1Nh1F(N{|@!IdjK`EI9Qd5;|890cgh}4S3i<6k6iWlXZyF#SBiw`bi zqfa7t&uQX`8V+u*_vsL0n#8P<Z6%S0(r{?X9^hx6zpk@mXzjhu^MP^s34yRwM)ez( z$0p>sea2JxtcKhHA3RzU-aaxBcmW}|KNgtY(oIfHrM#(#Z37C_(ZVaZ0EwE_*n&A? z?>XgZWcP`jT_>u7;eOlFA8`7b|9sLCxHiT45(A|wKmJ%zzW*fH-zC)8u??>)vpE%2 z^XX}i57m{2f$aCIMF0E!Jc2Rw#F>Rl<z}uhWH2Go=Db<?A}cTNu=k{X2gJ0_uC9`j zlA<s82Op}S#R)pr`1s0~-4kQ=z$<8CVggq`pTFE+sy?PK+If0<J`K*z&mX|i367gi z;vWAH!Sw~;zQO-An7ju~>;<l{?SJ&V7q~_vxE43yL@yKg2|=ULwf`c!-2eR||NMq) zOlPO+Q9-ng9=OS-B$OXVf&>KF@E?aEOnM*xgiG_Mcfs;^`AC<s^qbBr>8GWoDAK*= z;I`K_ht6+I&97cnX<76d*iF|2M%Z8(#4(R^be{7CmnBZhN_x<h><!dDdUOeCeJ#22 zByjL+*4Dr=-dC0^o@01Ie~R@O(wqCgZ5B>6Q`_LU|D*YzU)pEQZp9zFFF)KS(Wo6= zf`Ak-U2yV=9uayA`g5RmYZl1yN+7!)u4h<u2!O#1^eV9iGCBC~4cPMRP;<TTomT~S z+ge($-_z$7wD2S*?Lbc{#m@>O<}Z~Y>a!LNuG<>AiJ)J>F(&w?fdSq#KYzZ2X1Bv7 z`Ns`#Lx%>8=bnX}xu6L^?AF>WL*3rc{p9LTh5?WL4aikV*NCevXVG<Va_$(1U3lvs z>*s<r{27w0`{esmK2R~t+k*V?Usb)YwScl3jUC$iWQJDHL%Zeydla`>_JN1oQnS0e z2Ye5W^UgNfzk4HVAJ_G$YN&_TYrOA+rNDpYui-qY-qq{(Jm~&*K+f^==~Q(~3!`n_ zU*@kuq=vpB=W32M0*#C^j_)=aAHI9{&gb=EIcK&V7CvG5sqjslz#Vd(lyF41K<4SQ zAItp1IyyQ>@QSQU^(RRO$s1QP1a~f&FCur+CgCNgbb7FmQ};`CzVujt?I%@K55~GJ zXgqxNb}}>!7-#)XdJGar(}&`zpodWGF|);>`s*)ErEA1h3;2|R^5dS}1{IM(L2`lv zG<dEKSLf&F?ZVyT$7dl&RxR!R9&CX>`%~iNuH_7+`ed^I7S<<rhIx*;<@If|2o#`< zG}P)E<^#us9-qn`L-!q^P~oNxmzhVK@R=D6S-;%!BUKa>up6L&r3;9OJ&$}&?>thk zEc%BW!QAV9XXA(v$-E-1yklfch=tZGoB0659CW6bo0v#@PKh6qEWwM*(b>6`7;F9o z3{=bTY+ew1b#J?w9P5@~(x80x87UT~EEX!N8ff?w$lHPXL5M|48-zKf#vFIc_IrlQ zk5*r@rpJ0*?3oc05O}<-M1?=blWY03Dcs8PS$Rc8CeOVbBG>#LB!3P(&2VrOYJa#% z1jeolj=lPq;5zWKHDFy^x8)_g{sv!>xZA)_;C8<8i%58ay`deihofBL+_~c9<m9&F zW1zD@`NsoN%J@S^BbV&o04x0cdw67IWME+DlK&#eS3^m<{knV5TH+*%!fMeucf$=- z>i0~_SD1r=?2vn3glcYZo0i`euZED6;%Dj$FpZLslK$G?e@=&sb|m8)5*u%q>EzT< zfh*FqVD+2=0H^4Qm0(_DjVq?`Ncx_L#e$NF>%!tggN5<n@CXX<6z}}W;#;J)y?i;g zxD33JWZJ>_W7sWFB?Q#Yty0%qwsG`8p$N;yrC-a(=InI3_&%+us5lD*I~0~gj#?q? zKz!TLTy^t+k&)4rUg{RIHz8S0E$M96%_#J&?*mTkW*w20z@c+U2pEn#l<J9QEC+@M zOGJnZ3i6|WLIF=s`u&Rgw_>PEZ!_(~WJb93Zgp}(1?j2^t`Cywhsctfx;aaN++iBO zS5{Ua_5BH%-(<!NQtLTZ9<4`@N(8eD3zHi`T)S2HOv1ezClBwTY`gtZYdLCKObiXt z5xmMT?oyU=-Ck92910pQu|Qsw7WM>loF#8rBD(`zP`~CqG2`nzP<sr}>ju|xE^h$V zp|~_`67C~iR~1f6XADzaGilntRvK3jUvC_inCX;AhB^KLSu<t4CL({uuYE-v%i|7b z)e48~AY5VLK)5^r^<jtw{c7JKM!5S>NF=7FR@}~rf4E3idtaiAr(f-Zgzl`EK1Hs= z$~0jWmP44faG=6~L?K%9hI3xWD+5`l1d<DP=6m*cb~3t`_bS|I65l?Q@cz~w*&sU2 zb-g!*rC0CD?Qa^kenx?QI7J@!q0@K-A0;UQ$sE}GGsoGgTD=aFH#s@z&+LdMQmFRI z*DYkdwfUte%XTO8PM)C&=;(PW5wI2jyN#hOXw=TK#PvZ?FEVez7zm2T(91bbxF|2O zxZ&!Ev}$lcBy!9<5TiW^T<5JQFv-wQh6n(vnjKfpYOy>!ea@W;xY{J8q?FwU;#;H= z&NzaXo4Y+tf-R7C!;6~`6h#Ka@qlwa=0cVDbOFCFAy)nrx`Q5He*Skq;xB__-{4@$ z)ftNy{$<@nXCDjo5n|si+nnw&Ov`h0ldyH_N|S4cgoYMaP%)NtA^TL{B*=Tvgq3@M zn9xn`o37WY+7HhtS>EI?XHf|hRmR~B6}ZJ?nFw29NT*`C4vRZ8BO^Rgqg*^5gyCiJ zjw=URh<tmv*;Y6C=J^Gh>EuHb_`CsVO!J)CqEOXV$%X~uct;50Ily;Pnt2cp<47al zfh-jHRvL9tL0F@iAEBlts-;=0eCN+CafWzh;FiT<?uiGqT=}B<*zC(|hbvDJ>pc9J z0JEbUTaWm4!I#M`mUI|Crgo2O45iW~w(Q#zjo_ia{)n8M_S^y^KP)cpI!HMW`C6FX z9inxu^M>{sR`j7`i{*uAS42Jw>h4l<a>7{$BzmUr%Qhqj2CX9s`PJnFk(^2}bU$0g zXoc4040W4of1|U)UgrNl*n7*kDA)I2^kIhX?(UY9h9N|d5HSep5CkP81SE#;?hr>* zR76DtMG>Ss1`q^ALZk#00TC(BHS52AzrEk@{pNhmdArtf2{X@g-`Dlc&&!wG>J8lz zs5xSkH|bRxY*N0@xF;EN8Q!w3e3OjU9}X>`pY;7u_t1@>=J1k^S=HVhg6{$54Ig0z zL<=-Om;8qypT-*=n1N8hW4&jawV28^#~85*8%0BZ0IBb}GI+N~n%1aQ9nLxyUPk#b zR}Xj+AU8*7l30gBiIBIchQ^N8wSP|DBPyY|h#@O+KDxB`7v{5MWs3pl>}d$}{Q+ab zoK76E&*v8w$HM1`@p4k6t7kwPV<|&njHnZ$=@E%7w(yfYh?%?-n~;<IxPN&(5${C! zkjEEdJOP!uWqbM)`6}2SZu9k+mb0N!FVRU2Mr@Cq+$?%wQYZ_DB5D1qHWlLq2HLe} zbv0KG5wnAD^NV*5M!27l9A36>ZZ*CSc6Y4;5m3O0+?Abe3b6L05X<ImJMSdom++Ds z6Af3Si|%60QOQWvc_|Idb88fRbdWgZSz!#^1@fik<pd!mb{~Kpx;6P{mw(GiX_OL) zD7Lq?&0V?#l=n-%onP+w`OEDd(z(J>HS6lwu8RtBraN0(LR<@<KYx)#4naj+voS}o z(@6`ZyBeHtDA2e!Av1K01Rs|XEN?VmEX)zs9e^!x=Xw=Qm$8ft3MP_o`9r2^wVJoO z(`YjsgD%A^RM-9Ok-=-(9Zm)f{i^LT#Tu!G64b^_90^^TbuZ9Ly`o#>XyK^ql;q_2 zlS7hX|4x<Z2BbJ3#idjQ5t^4oOaq~1x1jB6K@Fqm*mn|*2X6~l8LlhzPxn^e%FZSd zh;{^tWvSia$JwJ`s1!tOc}tt<an4kB3RdoW%d-GZ$1o_3RGM-9DXne)@M!RjY$=Tw zs%|~{2?ZYHGC&<-IpeOD-+51o2fV@G`VIMD&mB*{0N{?Gp7w$F?0qIosTLlEhu`mF zo#&ayEUT(46hb=`Lddt8BXU`x6Wz`ix>FI(a&Jht1V#4cb3Wg|r{<STk|W3cU=Q*m z6}|`Jn`KxUh(%Uo5);YXkwJK~d+p*Y{07dTanH7Z8J%sqPKQ@QWZr?J*RpUig$dKo z#VFugK!&J~yRWrDIFT2bXOY9^G7D02rPW*?DVYl}d?7F;g2+6&2{Ni|wJ!ONVi<6I zryHJ4{MKw{hT4o*UbAnUD-4XM)Y5ggUM6|$S98-|VDppdx4LgXBAB}~O&i=FM5NSC z#+Qp8Ekl$zI-4A~BEo_1=x!L}IgX92+ODo5bVCzHCHV)S4gvfK+Xk#fahf|?MDwLf zX9}cHS4N5I;L<M_O%YBx;JzSw9LjEchlVoJ)8l=2$#1vu)i$;tM7+ZNGXvlSRB3bi ze0qQ?J#$p~7aE3GB4g~KafJl?#GxsSa1rbuBi?|yGoYM<O|*Y__$@t~3ivUru*|!d zF%SZ?C@LXwiY+v>n4RI{pA1g`(`<TEJ%0&9#?G^yC+1LlgDV!ps6EG0Bv{$<5Z*>~ zz`!904U~SkEV#lI&BVy)N0Ek|K~^9)GfP}pR8&v1j^xjXe|S4tRg1pdEv+7Q==4P{ zxp&JQx6z^Z`^q>9KYPP&2!{BQ$dY;HGyl^w*G2IZ18UVbXX&Z+^{>E&M^yD$IDIst zuf>w|gC~kN#vk4$?frGdG$y5~SJ8@~fKvmz?GWVk=T-vm)$h{le3*T6k#gk5i>G@_ zW`cCf47mUi@TmtTEfTYC6nHu%X0&@To4k0@jd86_L0*wdcj?Mn)A#}xt9x~b$wQUS z!$>ZZhKqgH2C$<t)sZV&9_!)*VF|eS#KhQ4X?B2*letKHiMXAN4GVj@4}{69ar~Nw zd{-VTkN>1Bb&NVxre!irps?c5y+Cr8>~Btru#9n!0-cdqV23j(xZSk-xqOHu6}XqU zqEYy19#uK0diEh+VE@u57y!S5s?f1RoKT#R<mfaA^Msj!5iR_EDGo4Zbf0J<v^>kp z#-BLB{ujL3KjU`WyH))p<VW7T<v86C`s%ztvQK@BT4g~mui<tF5h=s|G%VkyO4cSu z9|GuVjk4JCv(RZIFJ&lvXKo9q2>n)W6)*wI%!+D1+DB_hZW~}8)k`WqpD|^N!#wxg z-Kn!$RDO#-nb@DrsTv`)GZQWlYL^lq@#$!3FMa-k%($D!zXZapU6ZC3hO7++C1+CT z({q<~+anVyLbc-H@s(fIo5y^Fq$W50cyfn%p<Jow5_4_m`k1NMKfR6O6GDhR<hVFl zNxS7j^^Hi)io)(87J7HS%qw~q$~w<OVulmzGFT3&7xBa*y6Q{U4(KRo?mcM_J2GQ) zk(qz{hMaErA$bX&?TQP>EaGAs%^%W9OxN90iDH<PY+@~ZU>;hOfMMVWRWSQ=7Rp`q zTYZKcUk-6Omt`<fhF(ZC6OU^{1eZaO<4^sZxutrH>kNi3G#+K|v}kdt`2sj-dYU=Q zNVRx#2E)C0UBkgQFLg_h+nbwPt47w=MS<LBs|6bjQmsX4?Q)`nturgsx@gVNp=k=O z9n3$S^CZywmWA9QJCW|*R!U8xeC(coYVXty$$MZtZ0}Oj)7dB}1+pe4qi?z&wy^iU zxBNzda4WKgm^=0m_qE3lH+L15P$r+cFsG*5a#}hdftx=Yf<PR-tNOk3IlNyFIO!Hl z3aqV=9E$uUnc|ta*d=e$NF3cf<~ZeRCbPdL?RNylTkQdq{f<s)u5PQaS6z+?BBwfz zQgH8M{&OdqS_FL((RJH1GMYE}BUyzxe%Y|))?=<?b}jtgG9M~}zAHfFHpP2L7y+?n zhH`jIHMPpPnv-r48YEVVS!UKRd_GBLzCJ-+a969D!OJjOAuh?nDTP5kRJ?dt0gLvL zcQH~D9}YQVXr80F^K!9B)vu++zZB0_lKX8g?%YJQWd46(A<J>*4Y5YibH2B+zrtqL z4laox_G|aRB-4K^8G9F!7)Y-|y0z8tk6&*}h;iN#CBRfV{Bb`S+Ccj%rt{bOh}FYL z$A51;$@6CL!a@|#>!sA*bH1@(_s;8BiR<Y6UC7jV5|ro<p*s)-^St)?Ffm#G+DC`2 z!M=LQW5;|!?Ex==@=F^?)Q9R>RWRg1ulvW3jTIGo&Fc>=n{NIG)Tt^#l>#CO5;u4@ zg_*n#Z{Gsd1X#QvKWw(?#`_@J8rz1THCCGOJ*^Ym{X;{tl9E-ws!sj7@b8QCZ<qCd zke`1*qK9^b0&Tb&{w=S5^e^P_=pTvQwfg_r3jVcpJ|Rd0|Igs2|F`|6UN}OEmrF+y z8|w|IwgjR8T>U&3f&ou_cb*Wc26_wM1~lMifXVuV{IQz&bmKQPvSyc-?pIQS(H9ao zx@Tu^Edajw9Rij>lmd#Bj;d(Za9Uo-LAs=W3ewFoxYahtBd%gWT14m}+}_@{dT`Of z;rrrZz1O&8MlX%R-6uIvY%{g+tqB+q+oz`-#?Kzc!{0LkmIz3v+O({PX!FRuRk5%$ zQSz%xfX;lCXkU4IPJ4NId4G2|<i-u~X}y@Q|2ncDc3M+YlaNQIW8=J<K56x^>_2Ee z>3J(FD5t*(0Yre`gBeppW1l4rgnWb?Ng4^LwwZ^__hNL`PoR&3SU@xk^$%m5-&<fH z1s%;1d>jc@q3+68KWq=qup!%^D^Vk-riM_qM*oF``}f7x65qUelW<P#0=!#3M!kU7 z!SDCAK0G2DGYmg63=Y4>t8#fk4nBu)1LEeUC6YW2f0uXpF==VjQ|6bjm{3&>QBl$- z?)h!^;B7QKc@m18l{%OZTgLkNiubcoe<So;3VmcDqpTdI`WC93{2K)Xoe9y-9yX!J zYx=0_p%)ZNG^U1kdDSe3wH{eEW&8(k!FeSvfYyK@y^8w?AMP0_SN1mEZMkfCOj45Z zQUnS1NXLYh>J@c8bZ(`z7Ls_TL0N6@cizLcbx1Sf^0~_kES{2@noWj186GnZ2~w&~ z<SR!oyl`25k|9#p(rP-Yw}XLNJ!SmegEM!o1q4V*O2ThG0rm!Hs!aTay_E?PpZeF| z-r2PiamJz+ed%$psI2kF?*@&4yA2dbqZ^wvXUwoeJw131-9^Fw4%`q~@b~EVTOQW8 zdf*nEcAf@j9GnkOtMICi2m>8*r2s}pb6Tdl5a?}B&z1HZMk3EkhafM7XIP_Ji-kQo zH8r+h?f6v=Y{GGv6Z`u52+fc{E>w@~)i}4y88MOKocTFB&CSg-<WUqN!otj~EF-d3 zfUgO#BcI`X`^>HlMgR<w5GF83kXC{8KuajrRT_h4h7pj>8>(X`iCO_#7DvoS+^0@P z>p(6#Nx-x98l=SV=U=dwZn9lfkL<NHHcnQuayXL>P48=g^rfG}vlr#C`K5&?Q|<Dv zlA`d;=za1|m|-u!hnMRS_;2fCKGr;4)fRb}x|PP@PK--NT4Z=f#hYBEYkg;ft)txd zUj*`<Md2F;8|Kii229rN7rp!`N)J+%)pu9as4RKO*yE|C{t3tZMc{i=YfSQzxEOFq zDEMVk%7lBATHl746R*%ler911y-2QH;65)5E2MN!ly;iLdAGa5Pa8U%cvSsx7qAYm zu8FL*0M!iCa$+WQf8|}7s4gbnfUfy$7mng$1AUZHH?Ug##-8g4{!6AovhyN#^LJ`$ z)Z=Qm?Lj^=KdwaM7mdHaQ(yv;zt08Ki{xl(YddIv_yJ@lYZgm`*oKTg<dw-Z#%{z- zUeJ!{Go~dcp)L9)P}w2>gpoeRv{Bgf=hoYn8syFyA9_^$%e&kZ^!`VhF!9S8Ux7<V z<eQlTgGdVxUzH5j;dP+Ioq(Nn8xEdV9+xvl-b5_ZQso01F+ryw4r<(i-oblvq2{!T z->;E5n;mgl1k{|9gwmmZ7K&_g^*_+>SoHTe1oy$)wxmf;$f-7ci5Z1Bv-U4!icOFJ zXL{>Wsu+`wQdi#aHZ>o1Zn>?!*ay&@wm>dkUceWvaoz3h=>19=G@id7D9NI<KZU{1 zhQH8|Ic14bV?e7A0dkyyJ-`hbug>Gz1wY8zzXq!+N%MV{76#fm>eaq0pTJk%;sWjQ zJ31cwdwZj@taCvdE^uZ$*%=sgz-#?b8B|s9DxQ^w5~UP`$V$LmH>!-#i@xkX3UUKK zLz$9F%?UyliM{jSdSCnrPk%V^;%FyU-#_Kk9#coi=*(b4S-GheV7RkSiD3$66^=vM zDPDAnX5Y<B*&!<ce=KlJqN2=8jxN6d6>L=^j!xJJg{5<MxEnBUriz-H8eoiKlxJpU zP<W6(TVD!vvS3f@N#j<_i6>s@*CVH5)(%|Bn%ejZ9j((&l^`iv(6K)nyLALD*%B3l zC5bQOPE5{^%+1X~&=RCC`Oj(2Yx#dBei+ZAN9;nV%6;I1g=l};OY`9nBeGJDD3LMx zO=fJjT(k}Vwb<#te$ZQ`FNc>wTl{0#JZalxhsHFng%JGpVy8(h!JS5EBf)nA0x)v@ zR1RrQTo)~3W|}ku47<}>&jWG2Zp<8)kd)Gon%(ldBFt%=O-B%rmweoQ4SyT0uYYNj z-qlRspf{2&OY!{^N3m>jT0_qmf8jP*#hq3p(rm7gQcYXj73A>V<t6Lk4xgKaJZeU_ z5}~$QV#cR6`kZnYH;L<%x}X%Hc#OXTm*S{I%DiNQG&1;>zoDbRf{8l=)LZ1Ct{h?# z&j%O>e;#`&cANmB8!<4cPreH9IHFSnVi3;M9uZ3)5X^mH{7qti<EG4NRTrU7{vXo7 z2;q!O&&*6TDbjV;cFOou+G`0Yo?PzX@EG??@AnxwwBA(Ru(C^@7JR$+RmvO{k3;j* z;^g{EY)Nqf0)No*xvx!^=lWs$<R_)U{9YM}1;o7NLHND}p{Mjuqh{a<<1#W<W2+6? zq86qVqCTZXSo)$4ex6ii>5FE;a8LdQCpBmiZ+A*rJqZU#R2kVS(DcI`tjNyOpF~Wg z8K6(tQja?+^^T-eJX1%>xyo;Ujxmc!oQ^|*xHiw@#x`4?zqQ>_;oF1w$r2fEOnF>e z(l7LJ5X{hZ7v<KCzYg-wiXZA@T7fI3?;arXZL$WjGp$n=%n{KfKXV^7NFp@sogE!L ze%Ry?l5-5XSS<2-SR{%5yOK-{wgXgx!4R}F6u!|)wrL#!OTCkP#1DupBZ~Wq@!Mym z^-P-?v1teeQ+i<5h;XASc7R=4q6?KauXs^aQ%j5DSl!9fHhLB$!D84zdeKTm{viwQ zrhzjG^$C&+bCpZiv!eg3NRUxbP^h-hzf059%{{n%5?H$SsDtS10w(tdS%e?A$TwB~ zh$D$vp5QNElI2H1MZMhl7118^2Et}cSA49DQZ|5(f$TX~+`Ufl8K?i1K4qpiT0zaX z77gxYKTM91b(;0rXRM3JPCggSJ6>fkX0nsex2hQ;M@Br6cT86HQlOE{o0#_y*EM`9 z)5J;M`MUMXs#hKmrm|Xd;es#{znUE)h#ev_G%~4OG4+$vZ~2@uFfSW2pY@QF5X|}y zZZ7ydKK2#UDXehOH)34z2QG3v&_#J$1HW<TvrjeENS6i{)Vur9O4IFcQh6@@gyDr# z0|846B^jIrrt=UZUp0>fTjZ#Eh+7{O3dMAe_>|KzYf7A31~^~U*N73$ww|FO@coZM zE+Rk0jifTfb_XqOTSwmTHsq(^q`?^M0j$gEmxju~rFYFV`bXW`!U7uYoR~}Nt<nbO zIFqb3q7_J#-K99IC$|(TN6p5`c}k`&eygJjNWyDCPMB{x<=7genfH!OfS60Q`e`Kv z23DZLPDV06;pz4UMF()?cIAO&2Ic|3$?v49sUL~SP8FCSbyt>JRXzhw(V&ooD+T0B zWf+Ia0LebP*#t|P=rYia9u+C=<_Jxi0>(1FOj=4R_WmLrXLThfEuOojLAu~ekVs8+ zgD?f=q!)=NhkpeW%6t5IB2#D9k#Z$;W!D#5(A|~B{E}29-r)R>!PM`>9#&dfCp%WF zI2TBEN{onDJ%-Sjek9_gyV-mL>(8MbaxS6y0%1^^!@6C{Uae_<A@#H`y3?lcng+RZ zYJ)Dj-TsRkQ*HA#J9nN}Upr92{P@)P-gz;T$di_PLPfbTaP@PI!|@nh#?vwSyp~1r zQ&fqj<EsDVWZ}Ib$Fx(LF5^8eT$q5e5~#Y5)<NU>BDNVqwkKa4V=c$JxEEIaVH7}U z)>`V}@``0^72QAjRDujLH$s1$`2D&%X3JLoGB<?tOaA@)a}N&>DK=I>i=tYj;>)`N z2IU$oTWdE=h8lkD^6*-Ba}<e!nD~v8;r;t?Bj%cY3?Wf$HTH;!b!NN&G;dz`om7u! zvw=8rv$T|Id>#qP{kz{E02>G4rMikMs4*}!RIk3?+Z(G@h0z&*ag1T2L)F6ixG<1b zZQ-wS@eDLRr@P42pG76pW4lyHv(4qjL8n7PMr@&HW=f2^t%5Ko7E0R)3dcRdu(+N| zA~dY*tBi^t2TN#$nW>Wn8lOXf!Xtd#$&Vx+KJ4hGz{!8Fd2w5jGEJFuRilQybM)9$ z4q&VSU7sj$s|&C)99CaHh>m5(j?T$V@X7WnSi{V3@bqLe;bx3LG;<whKO;!<@X zdkoy(CX6L1z(M9ax5YTA9J(vLe|*tnc5uCBA|y+^nd2*@T-fuQ_nGsa?F>)0KuY$_ zS>r&P0j21-nJ}(>t2Yy<`BLRqM?i|j9fZ_PUxxzPg&HCy!P%A6oBbRbF*RdhzC5=@ zFAo-y14CteP6N~5!U4QX`03`Uw@o<9aYEMlKTxWXb({@r7K+TBvml}lI1{KH5Zva^ zbS-{<TU^5pQ;{VSAY(z^Lm00F1c)}Vmz~IUbl%c%9Y?&D_VwMEkE>B|TzNtK4z&i# zk=p`^%6zUHQx_wDuiL=*XcZmu26Z3HtI>dPo6f^=)p~WRmO8skCkg_J=|7;G0bQrk zXP;hXOrpk@sYiTp{uW~mfjsNB1v9O!n}=%Tmd6t(Nov|^hnouuZ_;?Q3&K)h(-{2t zia9d3!MdB}5ow6c4$YyF=0u5VLs)GG--Fbl&*-Gw!ooy`2c1Vd>R$5dcKKo%g3~y5 z0YOnYdNP=8x}x^TZ5fP7V`k*lM;kX&C`-@*&q%A8{7C(Q^dk!oXBXN!VZ5c8)1odf zc!+v8Ct#SUg#7BEY`MwrcLW<un)FaeXy};)%t)O))ioF~(k*&UzHV<g%QpOzY%l!9 z<Eb&G4?>(X6CZE7YaD@&FPWXinEgY(5u>a^?ymk7rNg;abl0a<cLu{Rbj3ou-)q>_ zRfP>QR!NGJmur2CXiJ2dTL)pt7G|d{jc(72IF_DtF@d==>N~1TfW;<Kjy_ExLbwgc zzPNYc`4?VOv_9u{x3F%fABKHJG3lK19Jt6uDj_XvRf5dicalC(+bJlzCjebrAY`ya zBIG9VQXnXqmnQpO?LOavWkRO;kC0$%N3FaYDHgc`0L6IJw?Hc1cb_VH)2{gPYnYwS zI*S}6KHNk7H2p=R3^p=Oan0``TRT7ZUu<Rkq7?Fa;(b}`*voP;nFb4{>*stkv5z4R zp{bG~>t;Nj>*xK*XTn&CZ%_07EToJNQAKSm{>m>Y`C9kT_+FLCzbc=vkI%!n=Vdko z?AeKvC-v1pu@j_KbgoH6J@Q}|a0kaR@-=a2_<Mo}Aq$i)|LvNsKlmsG9nS6T3+_>F zKiz{M>Kww5L&L(p9%HNG6#o|)`V|b?q!sWp;4o&_P&dKA2Z6DG^6znJx}I&ym^;(; zlkTS#YQj~;XL^^$=+n5QjLhPM->-|s^Go2SD%?}Pc3$ZJT&a|9apL+92%x&;?%tG< z&86Z4bmAId?Y@0d1Yh5)XaBj0A3#SZyzs57R)c!_`rqE(+SFrVW~PQb(#SR|1KIfa zc(p@Z$(c)+D)}|YVB3btF?aTW;66R?N@#rogzJ;)`7MuVLPLnP^`sva@WCLZd4UcL z4OKJzidq5A8rroRYiqATT55TDIgFSe-$wm?r1lh)R#iG>{{a;4KwSo8l&i5~;VY2$ zsDeegs;XsH(u5mnJ-E1rUhOX6K8}@r{-q|e(`=?snye@Spd&jy2;Cv1i;xjsaymMl zz!m&>Ufyiw0s%eS`}E%%2x6~Dh>6QILoLhqQFz#-(;*iKP#I7Qv=DwqVKFhV;==Ph z-rrC4rd&HB?8=quPvqytsrLbNLg9fbWflUwe2`#->gQ4q6z>QJ%LObdw^aQFWo3O} zN&_m0HnkLj^1)pTwA4=+SpjyR6{G*>XVLm!(9!wxhr@pl=v*U!zk>vyL=rrFMPHnN zfrDdX$qBRowS2CyPeG^KoD*zB@Gle%ky!wu{Ijx)UjZP5yat2<PNFXZfGW+o(njvi zlZE;a(xc%5q+J@m^%ew59^+%9J~MCra9y_}$l81f-o1asGB`j&3H<MlA`)m|KuiV{ z(Zw34=yp;(;2pX64Ok<fYmou6lmKCa{RSX0nAzK{v&8TKc5x3ED=x#5nDLi%1zfST z&`nh5-p@*#B6D~_ivXOCfBf&k|KQD=gQro4kFJ~~^k2b)F#vZ3Y8xbHfB}97KU0?x z1SSE|y}0Y57jtM5SC6Xul25b(*jM3RP6a`ZH*NeMA@vB{_VBCmf%t`yG3VB;Ss3m> zOM#4m#*Gl0NCrnFd<0C^)9@bxZUs+r8kY(|>bCQg(XtbIM~8>$d@?dKeaEZDf!s{} zJ_ALY8o08oEbJ5vbP&w|D@QlXm|0=4MvI*;x3jfX58vH@=i?xQ!GiFs{rLAyfBh5a zLCcw(pX!oNA3Rt)5%g>A;l1})><J%ql*NC5?te_Q|37T}e+?iXuYywodWb+;(4<>R zK*+-3u;>QSIjG(saEcp3)a+p-P*;aUzSogdBKPiDi^B=43d&;;mHx~XQ%-f}9)1N- z$5>&|Cg^lPp+I;t`!BqKG!1ZqN8JAPKPcZ@n|<=<AY@w-Vj!oXk&%*8^hgr;8A{S` z2vRp!JI>80)Zsm$z@4CyM~;B}+>Heh&^3Feq-QuG7(RWC=Hc-%IC0_=00HA4Ty_Cv z-Dq=3bG>q9{gLv|#s?1`B-IIoM*5yS=ldVx;QZqlH*^LM7v%78)fL0z4Xd$pH_UO+ z4zRsG1<ik@nFQz<h~nO^uqtq{`A*i%!>e?*(lmMoj0{xvGWM_QCfZ;@x%%n#FR2<f z`XBT2{y>`)R*`&_0a+r1W`jr*PGMLhQ(0wBnVUZuKZ3Uis#CUWGj4;W7hIvU;c-?g zLhM(EghfOm0L^~tH1(kBbr{&j;JgfXHpah#YZG{t@S96t*{lu8IK1C0&(}=&0<!H( z{$3CShHzM>@oJUSJ3fRUSxkIfoFA2+1@z3))hcGdbnV?>!<sT}_O!+>TnNhTrs^)A zg{=U7ktjB@b5YFyg=yiefp-y<Yy{n#pUr7wOT9~<K_{{ZS$vxB?w{sEN@C%}_7{cn z4PK*#Q7R%%gCf}Z0Gi>?1oaezB7SOuEG&_G6K!zcdX7~NKGJ$K4WL;+Y8xoe7N0#5 zcHohp!I;>Mq}ozF4+&AmMuO>s2N!L%=Sj8#`cNxUPZyElzsm!~WAs=<or8^76QUBQ z(N+~IJj|j$fB*JK0&$=*<zTs1>E+T4C3+kMZD<cBHtpQgw{oc>#=b&67$Q#}{el>C zcc2NrBx^71S|Vo6u<;J8Ms2bkC@&ALZstoF#Qs&XR+(_jQQr7*;!o8}-T$6?pj8s% zM@>R8=mm3y0F6V6qxXG%a9qKoRT>I;)99jlNUWgr+eoXsazjS7{qF8NNv9O8`Y$z% zX<scrAlqO-ClcB}=Y^ECx%4U6xUXwSSFJ#uLV(<3a77)$HC2hRu_^XX&ScLPEkjDS zMja_rBaR|mJ`!&}64AhXG@+dB=eRti`Uf0p0ITO@Zu^5Meftch1xez!9e;4<ot-oo z-+ph-6=M@bR3~wn%yZo$KC08XarhF}6z_S{!Q=`F?%wN9TVl$={p1)E{6H_I$qs&M zYu@Iqq-2@Pk+lBNjo-9<{s|;GM4rjmPk$sY_Dv-k52?C89DzGl;(65HBM{qd$wMoI zFA0u=bKl8dn&;V#aTp~a@nI~>nDlJz=g=Rj#+<u*dnKi9&S=lGS}H1q3gLHufw=Sj z-O6h-w0OIUg`MVuw}!W#26ghCP1uy4X~?!Y-p{d6!y%z)`ggckJ!Wb9EwOtU>udN@ zv%jd2n1&j!J3h`-x(*b_-hSInTgmJB|IR%|#F!glVS2(&M7iFtl#AmN5_q-4C!5FS zKKxn(-#ZO8wKPGuS-cMN3z!ZT_~={iT3c8kw(vqg$6(P^VQ(a0up{#F12_P_A+~`V zN(rzjEqgF@7CC0}Xcz{e@nFy(gag^xl@$j<8Nw>y{@tg`AQ_Y|AQ+sH)$Jk{Ay5vK zfBIINI$Xq93hWC3{e+N^5PU%cGdMhCM~KZ5PZ}U1!Ptb$PNH9ZrW+WL7=z@lGdem$ z5E-Y-+w1u1TKsD;2NA{$#H<sot^Ka^B6nl>pH0x$zy_;_^YnqiPgm?Tqh(PDID+5H z1g%dpG<5eSZ7r{YDM;rrLIdZDDhChGA+*U(5vj|_q+>y$8TN7K+jOQXI->&`dCADy z7bkDCDFs5G>;-LQP?k2-0ZF_E!wl^9x1M3qFoqk9f(I)%u*^ZVI*3_7VSgn0F(_q- zvbWi=P>!D7af)|i>^e8;4|Y|J!<L)G>AGV41cMiCIWZWlgVTC|shKV?`}l=1Vt&rj zMNn58;NDfS-q^(Ox*y*RiHQqU|9pEni^98A-@3)O+Gh>AX2|$}Q$%@dLS{2@y<%ZI zmE14EGnbC-^Jj8pL$4X~+UOaiu9{<od}la5F_$`ys^l)!_WR#G4EwiKDN|EGsF(P% zT5gqA1*P&1;x{CWulEcSM($59Plt6sU?`Ax?UHL29j3u3QogI8zHC|yMX@Q_YSZ3u zVUgl&c;+P|?m8}IK<w)!&y6hqDex{WCig<Rs<^3s+5Lqye*H^FzTNUZ6}zcymkUF# z=_q*JC1zz;E}485Al4l>q)p$^w>+o2R}Z1t^6_F{N^b=%7;(7NLnB=dq0fn$xUyzo zP-Or1W%8(o(iNVmPE=9mu491Rx%B^a$Up^$fUBPyOy_gt4acv2>G7DIF*(I~{7O-i z8+^bbBG_^ublXzp3;<z+kqunD{lq_Nj<RTGjY30tspaKDA>+g!E*RQWI#tk72e21% zgnfS#N`ISZr9^`U*F~r+#>p7{1d?Cjlb!&yPGgp@&xGD@6YPf?VTYp33v+a|tkTv_ zc5U<E5F|KvGiyZ6*s%#IPIh)KUS!^PHR%nBJw3s9f@S9M@VZZ({mgeBi+Z)~?JhD+ zx@M7(X+cs*AXk70rbS9%0#rM?QQD_-^pkF0QIp6Y8#$j3yy+AX=jS+$)k`q-+6=U) zR0pf#T;P&P=1S#W>2}tV4MMFzc_nAs0=o)Eoyj@|Yr>7ErzU;@{VL`7=nEialgKl5 zTvyS4MnbdLE0`_t#z}!*6TF*4NPxAhl+>LC(BD_GPE(lsV<?FX*>8I@qu;0pu3!q2 zlN(w;N7wkt1uU7i+^hjYG<k%G!}(;`DW^dz`~yM&-)M3y(2tDta@n+fg=QK-Uk&~M zi%k^o#^w_n5tzQ1)&7A~cdRyV4+0&(zUHCElxD2Is3pfHyph*{u2bonG`D2SwNI@T z*><0ufWnYdGG~<Cy!CZdL-q{TW#=nAaHB?Tb2SpK!8z<$Qd5!hMps(iO%}^Jqka>x zUf!D!XZeJdSF`AK2#^>z3Ybnl-rA|0jreVE7;U-`yyW1N*5KH1hXVwoLxPGAAtuc6 zY<y5m+|4MWh#|Oq4US)Q?PG4w*p43;>J%TAdG7l+vX^_eypgVdY2tym#W^2BB=7&A zLPrNQ=jFQ5)=qp(<>7}<a5(I%F^5T7JopX1@M~$^L|0-ehrw(CrAO`7ZBHv;JJJh* zslejhEbzBL`pKV;JvlJ7IN_@J;HFxAB$kwKF>?u_C!xmVR{TPgwwnAQi-=Cev&UJ> z`RaHTD9oh;$}{v?U}EyOE)pL$aOM=}4o=j(y8$`OWk}da#R+(P5mXGoiPaCOaBy-H z6m3x^pu((R**ifkfo=fQG<HMQy7wB%6OvBrujFQ<^pnK1Pwg8EGFu)oVa86X<}AP> zLewZ)6Xy;@k&@YoT}C4pn`;o2I^ELn^5x4hUPdwOOs#urdl(xr_5}59Yg9}k7&dZb zlMwCAM6@BLi;y#Qq;A(a6@<nDJi=c56004%F)jot;P-hkG}Ia?KcrejCo!6;8)4Xe z{sa&YM(oZf(t6JmujgEozLmrnbo**z=bfD6)r^0kXnZ_aMf0o~mQ^$U9RT07$&63H zM~e40`rz>F0(R&5B#(1kXXZ9-MtB7F63K}e2EGdswHFu|_{>kGQkOv|Z9NR!!o#3> z!@1oUn@+2B<~a~)n^yk9QrsIu&j-18g9L?v<)?E3SCdHpoaXGQ&DD4SUtC@1Uy4cy zP{ftZSEd*X(25U#rs;+)^xxUWl^z*PlkK)x09Q%TmoM{zbL3~Hs0gbjQWu_#N0id2 zh>Ya-ea?uQFMT37rd#qfT{_5V5pwI3OY4xpz^f5l9f>W_!b3Z%z0nGUf&4<Y0bc$l z3**?unq5#-?TF#6fwbrIhscn6Ac?BVkihI;??b0<B2ubEbv3YmDj4Fd*^Qbpq}K09 z@c84?5?s9HnxQ$_*#f60?iJj0eAcyM@dUsg6n;~B`X@I|;6y818ra7p^0Y{6IHd%c ztc|QnvSik|<yJ6^`k{N<m-*>l2c^8_!HFn97ne{WMk=v>s=Ybj(?2-td8S^lY`7}7 z(aU|`{F3tsg%2b;D_v*AZa8seP{q+cpMjwqU1)R80!x(REMmX-N+J6II784MmfpH` zI<G}{Y96726yr13fQ74m@W+0%1d#z5vzZb(?p^Kjt2+?Zs3Dq0UACQ39#i@hBDx#H zU0biTvJv4ow8(DXzRj+L41Q{L$y|r}+_a`uk4H9-j>Yjjh}f7TdMUgE#VehkiSg_l zikGM<YC0I(OF&rQ^V}A$v@(fHNJ%Gks(xa94KePO2L-CVdoN1^)a(DnS#%?7ZGr)_ zh6MwzH?YXZWbu=RzVIMcl=s(@H2n+TqT2^53JT{gsLG<ErR7y{BR4EkSRA>-kR;S7 z1{q@H@A@Dvb?IGF5;<oR(68XK!N?zJ!I267w}J1LmpmUl0RiwpJ$?BV6Q`0yik6d= zp@?lyrULZpWSPO@PM|lZ>9V1@Ii#V%Aas|V&yy`O|9%eb8ilwUmIM=RA(z^&g{3MY zk!!Ww=pY!NzkfX03n(ImtPu><zw}K_sil)_=yP!3AiHTeuUR5w5KgQ-N4F}3NB4m< zjHWb@Z+4g5E!J+AH14s@^R6t8tF4Cybb@gyPM$T48i1R{@vx(&SB36${e*xZ4|?Jd z9-T<d_HO(f#>GW2l@fL1?@>fXwCxo}FNsW(+Ozkj_lC-t&RSN>J~HBp)tUw|@2B zf{2JnwK^`~iOXOEfQjhpT)mDg7xw%1NmH}4d@*H^I%bCV*JEa3Il*lmA@5Mb97?`3 z;_ad*tk%vC<!F6~Qpd2|yxe%JJ0-eOnEmRrf=S<RG=A37rLGY|gC9R$yl|G9gC?6R zM6c}c!LrVRaGe;JQyRNyLNkHjV$WBNU9DC*b;<;$;pF9AW0ngHphs9dO(W<-nLuil zta+xQM)T*rGydg@(b3VP8htf=E_0TTe~_e*lnH2oC<ZyT*FzL4a^r@0du>(_%2oHe zZN1ltV?AV=0?Y<8M;|o4=7gjvs3qho>0UoLI5;{cmHJEWyz21b{&r~Nv>CDu3PYZW zS$XoCPoEysHx#vN3{G#2Z}U$OsI-4>lGvD>eebQb82ibv4jJP}g$_^&SSA#W(f4M4 z1eYQ4t<viyBJ1?@>P9KG2H^)^C?0u#xH>2G^89zP|4`dWul_<7pWLI?V+{gercUq+ zYebPiKpu{dF*dmvjY~<PmkZE5uX=fwTq;JmPg>MfPx%MWLQ!$4q~1qsoNQp*$t-fR zyX@2*93>%6$!J;Qi@T<w<dz_q@i6)T2hYOYH@tB!)=Y(S?9-KJv_CiscmF9Z*vNg2 zLke2gQc@`U0;Go}{4+BRur=%}M7VjqAq#sK60eq@a8h@bbfi3LAYwShr*=BE?DVNq z6$Rn`>9$H{G9T`-pjT8S>{(wvYdMK4+bjxuu~=7?vN?I5`nH^J26~Z-l8S=7aDyXJ z!c@G&*f834OPfAC_kK@LGfjI(qO3)c3!}J~Gtp~7g^(DD<kE*RA4ku+^oUfQjn5@! ziW1&@*OZe-JT4#3+$FCr?pS{}mUeBBx@`H*n6DqiJ~@#^`C-cMS~5IW85^VVDE<9A zTXEA{!2tWjIo%;krh`%?DOqz@<+J1rqnm)**S?5o#X!5}ltdkc;GIVO9H!W9lkYr+ z7c9@zl30Cfy2IYo=(|5uHhoqp&nFozan|V*RSvrBHU($JWy_lhss*>W6;L=d3MDR` zoZ_m$8pucN@rxw;IpgskOLsI3^uMe2ZM`KjPkH&SvnOcH<QXxC#?q7QeXH-}3Hm-) z4ddEGrOyxN=1yms;;26@Q}YNY<aIZ@?aP?_di&UNvFpF8(LE9j_;OZ%@95pzi$dYP zWp<^55a7~_96)nNxF3O3^?wjl&-^Oh{K7)G^DhMZ9nCa9I5)fv>>Oy=`WjD`$u2G} zg+bg9-2Gz{r9UFse*dp{kpFvHQ4SF?vHs!GCV$|5yCHDVVM6MZk&&DZb{caJJjWy? z#qY)5T0o8nK{7jgZbJ`#Do9g$J5SSO16gix8XW-qrF;yQQAn^k*pVfmn`{3uK67MK z5Tt!}&aZGaH89QWIQ}#{ecE^QZm$W4@u^b^I*T$f%FqVpXcb!GJyw}MM#rgQ0Lo67 z<RjaAOG}f!zdLAJ**kA<9|0djEoZ&;C4zGlUNz-M>p+hH7jSGm1>oH#k^g-9hVZ-x zGhp;zgTMDVdK;XRWytGMOD#j7Pref42{`!0HOX5**4C@Q9c=4*0Fkz5+lfjVf?&>_ zr~9#JtT_mLOo=0zQpG3uE+#1PFqB*%1V{cCkxf4dz*XLC9xDqQ+eaOBhzxQ1js3d- zxQrVl^?}q<((7RF`_ue<>#SSJ!Pb)yc%bW=bE*p6!R<FniwC%*vzZA4&>I;ksY7G1 z8M|$y$<!`CBQ_HBrLZk`-g1=Lzs3R)3y+xyD7I$Fqq@ky#TO#$h3oxzBNy*BghA$7 zc-AZjD;A1dpSddW<aG~hC)pnC*WA7PVRHwx+*Ua`+!J(mOv_*YX0Qy-b1h!Z{zxcf zD!V<QDD(p9^JB*rpq2_B@eC7{D2BKUEvP-q$>cV<eiXGZ;pb;(Vc`_1W0TPCa{;jE zNc4ZvG}!JNuPye$b^*^cY1cO(@MmXr`1$7m%CAgA0wI!_TMew;fLj7?0C=UGpr8x< zp&gG3c#0Z-ug@O<gJgUgWLbdYLwpE4<47?u`_N&eM{QmtC-CcoG8zvs27FVgDJh#f zJ79WR2K7;rj>QkyFSWF^{1=vQMVT8L%ex~hzHbi;E|}la*19|@TvN0FipYKu%~rmu zqRpa;gJc~4fepybGXCuDN;|yS1ye2%J#F^@)f=TOHvrCV)JdxO-u?R;(pc!5(}DP6 zfH61HEdcn~@Xs9g77qoSf=rr0>OsjIQVFa+2o<}vu5i|N(<Bw!g4w)PH8L1tuI}kU zdMC(cMXJtG!^{H4i9l-1tdU8(_PTc%F(wmw)g1zAK&>}q*Bl-S9=mNqy7P3;q(^#d zD5+0ho<v^U(!Ksb88x+Hfg+u#BgkBa($qhcr56ctp85YBjG}G)wm{|xoQ<80%~ZX& zl{L*KI^K*Ugw2n5x8*4~92dWQ!4_pHvCO;T#_~GI=|K@qDxnu*-ELmrE)N$lz(Db7 z(4v~3w+^M`dU`QLo~pi}Y+xro{SCP~`%(^yF9xn5g`jxq$cxT-d$4IDPXF?mVJd{a zg2c<;x-|CLKxoUmy!%4%r|wX9gU}T%C_f2X9=>1!Fx}o@a$Ig@tAClw+elMxYhR}< z6EJWlWoCjD8_iCGZ~xBB4yAW;G_j&Kbmb?cxiA;^t{v_LZ6&d(irOLS&X9U3sx8g( zWfY!b0jCm(ntsZ`Tmll-919ETu;4u=qOMMw82mHWlc#9e<&eF?pMaCe6cP-85}IIm zN~_@%xWK`|!7XP=y#g?81F&4OtS64$ItdKguatjJ3a(ht>du+0_yGk=*c45nP0f=a z?1z!1mv4`U6+>Wg@ZaEd1TSMrem;q)tZf~H9t;iV$nS#ptUuRwlg#`OjB2}J-R0#4 zHv1(SoVrcl5SoY>pE5Feh$-UM2;IEf9wtTZcDWD1!x0#<yCFcFR)5X}g)e^TL-Y`= z{|R>o9Z2ljXr@&3jx`WtP8Z4PK7s0&Yk*L2WAj0BJ3KTLjPkskv<X?0?uYn?XT?6# zb8>=hj21XQK-|pzm#F<r<8B9R5${1<5&P=IV=nC)g@+XQz~?aIUL#$m=mdv9>oJGv zJ16N#+kE)2yf8+RRWl);)eNCFDBX|5cv?LBeHm6YSkdkU3DOyY_Pscso(F<vpxVmQ zdhY?8>s<fgYT1b&gD0c}jiG-7Hrq4s&hri5)KGx{yXQfZ;mPzqXnLX*5YCdjt5?gs z>*AjPHF%FMy#MCrLqH82v#A4zKOZ08P#u&OIbS9B0UIk1j!o8XWp%ap9$X}^A)Ju_ z*OS4T4qvXa`#*$V-J4W2oeg7ABJ6Yr5o;4N8H6}<OCoH!T0o`Uug%R8usm*;48^<( zuS3@<2_rmUtH<;oT~S}GLDV5GhW85)HSixNpx>aZ2d@UfjwP!lEy1A`rZj2Gg!7++ zth?+_U*zNkM3|{LsA<-Zygo+wG#eQ4{Ukg64>?LaG@3N|SpZ3wU_C*2{AJRf>&-2I z-#<xHPUh#y%}ULRyZg7#OOE^gNA@UB?&332(Frdt(KLtxV;o^;rlzG$xDVbpXL_1u zLb(U#39$A+Q0S*mO-;cUt^;sm(6^7-KmxZY_plpC9XpnI9n>U{LrF5z>W55OIYzK> zGXlPrO*n-~N!#)^>;eOo-jMU@cL5-^->q}xJjt_k5!5FyUtWifpDQ%q2pMS~p1<8; zXS_P|M$PB${rj$be5z>USnM{{K)k1#oa6q2<}~$lL&FomW-uh(eg01&+TyFwPzX}Y zgi2z-8Z<X%;5Gys1*5FJAj(Pn$><?GaRm2dN1NlckU@)jnn^go)LUPAnHG5K%sA9d zOyS1$nDKfEPvS3L-axWhAd`!s<mp#eXZP~~r%?VI_*D%mx&yJLx*2#pQRef69YV<~ zOeyN<0K~XVO6o({&kn#aKdWFfa5(;mnq961;-vDCuGM2}+!NH+q}>!t>2*q)_u0}` z2e$*+_nr@H2iQ`;BbzVvAG(~k_%Wwo&A?F9FLKtvs?-*@eJ)9(yC*aGN?)GVJpAz} zxV0~+N#WJzpF0+PdHMNG?{d%VjvuAjrD@i04?O$&)z&+@ZRf(*s7zj;w;S)zTbsXI zw?7}G++57wx2Ec~_t!h!>ZX!`wa|qwT-=ZY?t@p;=!Dt+gx7tmn}nYu_1~Z4|K8*V zgI4~j#s9Bci(dOzRaATiyc`50zW60GlWU-vU4Rn=B5?xhVRDj?cvx5W4k}V=b6jAb z&U*{P%i8`HFt;GVfW^x7kNcW-_-;)lUBgXC`nhKn{P6W_&gby5JsiI%3qit7wLio1 zK<Q9gdT}c}GEzGoLWy5hj9Ag@LN0KZH)-@n&ahk1Vdw4-Spr4)EaWyss3lzlKd+S9 zN&`ql*3LaO0lW0HuG?y<^2M{nG=Ckf)7IPgy55Z+Jvd4tTt}rd(kC7s(Z_s+SLG$F z?kb*&!oqHoHO}1U&RY@4HFynx7iDuv-~t$zWH_Bi<M^>-!w#ui4QIajRfD}4@QSBD zNcm4RKFu1JTZE4Qz&-b&xg8jl6%`aA3Ucwwj>#&h!9ef&B(&TCgqPQ$?dRzg4lTH# zCWaK6K|S#(lsD-BbnhK0i$R<b&p!Yw1~ZLgH|h?-%)*u<f0>Vy({|1Jjm5wr5U?6I zcCMiN^#?Mq=!!N1dPkViOGM_6;Fj{g|4v;z?LFj~Z@ihf(D1$)nu7|%$UlFvMtAPb z19l&5ojS9=zWzETddThlnzZV9A40lvqcPEkZ$S|AA!1UnyT6~wF5lMO?EyFt2pfTJ zhCYz!Sm5shvPw!`yL3|0;%GuyN#2<kE082W20XI6Uhii(jhvq)$ILeP-vOP?(}Hoh z&LQH{2SzZ`m!P65n~s#=(F&`R^@RIn!y6R{$EJ5?$?3dgkH<*5_U;_I5Ji9sKmge5 ziHZlY5=dm+-7SYH<M`i*Vic!gukh}<v)I$qV}<s72OlRXO6g5w=+-5tPm$MPT2v<D z)UFhMG#Hym3xWdonb*n?#X$N_^PKtV(_%#yzV=tatyMRV*gtZm8G%)S7zbRag5O+A z*r^<Qh~)YQ1XGc~G&aFhI~Y#wN+8b%IzSv?YHl9PgK1xLdUnX@fyLT-Hw_}`AJL@L zArXaMC?z3F3yZlB8mzggDKk~j>UCnzoW~H0LCE~i1Xu-}URC$s>=Ok(u42I{Dk{P; zJ|s!*Q$Qqg14J|qkK#dzK1I9<V?0be{k?^#72EvmZ0LWg-0Nq@9!DJ-yQ&TK_THwt zR*gLVM+0{qY6U);l$Ud2Blor?a`_N~Q?8+*0j3;X7G|SUr_}xCklgEjMoF3@cZCaK zo|PyqEq)GuORChAV2Cuid6ULb;SL!I3A~tRW)P)L2)oG1%Hn?<unOoa#85227XiuW zEfP!ij*gB~mNrbdb|FFWfUoZ!5(s>FD<G$J<LA%lUT}d10c!+0G-z*=oo|7qRQ`mF zUlwZewrqh$;EGf29{i(iaY4Zt*t8=W+rI9e(RsU4RP;?F7+6IO=?5|XtF%Xe%D;pe zF!LjE7VG6y7lZ1GinjiKspH2B6xhYEaq+kjlCUO^Vcx%3WC+8ediojUcznG54Z>Bx zr!L?FPfLx^mp}no+U3>y_6iP^(Z$Hf$Q`pLn7eQ9m)+g3jsnd{EXix*Ne*R0%DPRp z5IIT76hHZ96duyV;jp+zwZnt(4*^>FpaEv2mIrStPzzvkNfRwdRttJ+v)_|UYG2uR zufxE$wH=0SdE&GXtVr3zVqZukaFBPa3z~fYzL^GtlxW^wuuR=hsE!MR0ax#Go`)NU zP@5EeZxZ%7QFn&@+sIFd(h+|8PIo5JS#Hr(;bM+3Nk(qzkKHlb{CDGG@<f>dc?ESx zg}pQZp=inLp${$j{_bHe56)Q9_WtXGd+{}j6ND9rkC8D}>16c=Ut!NP2#^^kUV+1x zG3Wd&*kz>YR&OGEa6V7phi@5#Fb4TO%S%fz4-)8RtFxBeuznUT2w#=Q1ec4xNE6k9 z-k)x=1@=O6<#-89c@1xSV$=T4O;vwA!)aquT(84-n-bR=BUj0sE@>2-v~^ZN!4e3- z(v*pws!ddN{d<bM&h7#1-|a(!Eg~&?DRk%q$VL8?j{$f9b)fgy`5^bHuNyi$2)*2G zlSJf2KCsx)U5Vz$l3>Ao77I`PROwRM!Bindxyit1*hW>+4uI7Yyh<JCS7^LVn=Io| zAS;1mw3}~^?i>oP630vuemcWAx~@#VV`eQJqL0p6T8g&>(oR7TFx3<UkWey>6#^;$ znL325(qakh0ng!JqWS{^ZaRO`$G|wl=u30@CFr!f8!u-g!3-zxlQ_YXq+)Rt=B;&n zg74f@O<5AhmIy-uV6`?rkHaOFj5b3=uumV3SKBC7(X+0QQNdQtN)<As`dPDvw$_id zt|I8O8Vph!8-9%p0FKY4G8~X4AHCff+LoyDUTcr)e0)PkEdRBwC?8|`J>&B(F4e-K zwvfqf?N;-SzEgsQSy-Tz=u8E}xpQf)^PQ@6^vrp>^z;@>5JBfQYQ-Q=o`nCA`+TPH zk?&YmuxAc_&59Z3YHcV|+);136%qDZ%`^mqq2Alui#c^}Fq%0jEv+L|l7^D9?R*V| zL)@_T-ii=+*GE8Fb{w<>5i{XoTX0b3%Aq{!%wrOC&+_(f^Jt!Oo{HVtHB^8;#Kw(u zCrf2tq7p((O}HjxQCE?QM_(cG#2i2Bpa{uufefB34$j_zYJTW5`z~7L^en<Li`Nug zv#`8Oh8yerg8cNLBg?TeK<kOTQQFD3a*zkF6*-DYZjaVy=@X&th)-;Bjjh+ioPIR6 zE?VNgY4PVknz~qSq$E`6^67yoT>9v1;FfjAEiniupQnO@E_7EXF50`u9*rq!wqEq7 zwlZ!Dn3@XbCz#A~%G^!Wif$A+)D#kV4sA0C8BVSb(U#`VFRe)vd45&C|0crmk){nC z{jH)To^)q?v4&R=H@txUy5g`Vn$OMMoyg&unJsbS`gA>2#Rrg@!uKxlqN+-=>jbG& zX?+zVvD-l~s9An-eCp(=0$0;4H^M1h3N4Xe+@9l?VT-2U>Oc0DTBSvWAB7tr#UXe5 zSOPIJAc7Iol`194lgsM$N)muTbO8Zp$mitG7=&$8RVF^#$?S+Kt!>PlXb;rP8)Xe< zuKrz!QUMJnHqanelwPEW!jB<D#ODyHllHmom_Wmoptwy!blZNeIpWyd+m|m@{t`*t zN4|k4hVo<e$7CbT%WP+2Lbw%(I*8UM0p`$VAZ2nVA^D`cCMhLFgF$=MoXH$CboEce zN2e)qu$lr=tsd4fKP360`6(usr6am>#OvBX-m*v<^eTrKS?$cdaS}hv_8?nQ)U=4- zk*%dQu5c=Epio^yW6>jTz#)mQ<8R9-Yy&0=XW3XF9QYdKSeX8~G7;px-`t{L7TYG# zLT!N9PV*m`zAk#~KVdWv01_8gRMd@U(6Qc&#U7(~&9KbWh+kfKQ23VG*%gV~4nr9y zNjyh;PhcyVC=|YF1%1s6-{Sv)s^ymb_(d$_DpE2%$L!Rn0=K@ywGufi0oA~WAI2kh zP}`O!P5ZWsy)f|69GAe{ND4#OrG-*g)G+*@<xnt~i6>oWqw5jz>Or@Jt==nS$6lS- zbJ|VwFKlZuJ98#m3Xv&{R-^24!A0Q57ZJRl7&7R2_4$*#eNS(~y;~3F%*x72xc%fF z4eTv(erJxWw=vO!?i$FfcX}TAh1jIHdZyl+Z3uHtE|JJ{%&!E&AgtH8l&!lZFt^kr zIKQyGoG;B7GEX@Kn(z%l27<<q8lv%-yV6@o)gF<j4O#{njM9_I19(wCKvDa6N@}W_ z!R~+rM!$VKZ?Hl(EG48Zctf5vIWyvVkuqfezz_uhLcPrzM^7Bsm?95<y_XZY0qP8d zYve0f@aDKgKoal?lvQJHpD#VPw)yNEC05^zlsqZ?B2Sj$MnOiz4cu&l7por?W<e%G zn2K&rQz7ojE!g+D&0Q(TlZl+quEUHfyak#v-yDt=;If{CKaAPhiFo!Yr|uk5@|C!r z1ZVsLY-;wDxyz)upM30BJw50AHgmhK@&Dr04D8{>-FB$e+bou4!hZkD8?%GJ9IJ-L zKj6b&$`JFowR`FN2O>;vdaapXiz6B@r!o`}L+@rRoW?W*_N~!RS_NrVhiD~_TWAcE z)*q2)`v3dO%y4HN4nomwit`ZfqCkr7L&)5a1{iH2#m_=oOUphXoB`W5>1bnf`MFO< zK~|R2bJBOvGs&)`qr<2E>iQ4fvM=Ah(Zsa40fd_SsOLBpTL{&;Mfy3$GWQnJ?uWF7 zaXBDJ{`m3ZZLOm6XTiNWB7m}BsNE#EPdg(eG!K}s<VaO_ySmQnlz`2I%#M|n6*3F% zLw*Zx93UZn<ROm}s{XcD8SxuAqjcQy$k7c5pc(N?I>~xoe+Di!6`JmEpxth94$<}) z;+}@#GB1cf#Ar*6xH--Q%CqmKTgToLJzY-ygkXV)_InG0D6FSkj6wrtRErddraKje zoWcQGC#IpNAScJ&17L2+BPP!3i3kW%=LA@j7)kunhGNS6kUp|-E-o(jV(Fh9`a?b? zn5V^#G>lEqR7iVv{q{xJ$c*hfh<&tk3k==J&$zh>ztZ(!Ah;*yp#X|pUU;Y@1xr5) zg7s|44UXfN?IE}&naiw_iu~z72Tlo>^>u~WlhL9*!$#9A4DVKgxwG;Uxc^aW|8lLm zY;yq%Jny?CcZF!t+}v#*T_Ks!WcQSxj2%&b4}TlSlA9={U-vJ1`2NKM@t9b5Tet5I z^+Lnwg$$Bkj9IQ{jbBs8Vh2n^s4kGY!S-XO-W%OuKyV)P5PvX#t>7^yGNw41#&4qS z+6@Y}?d~>sWI^bA3d8@d`TGqsozKXaBIR6y-Hd3MK^jMDVIkXDg%7l{xkz0}9EaL9 z;UA-H9R<_viPKxl-(W)Zeqt{aP^6X>*CpR@i+@WEQ*m5Hh3OzB22~}*|Bl3C;jw?( zl49!}_RAZf@A<56Y)nCsNtYuxnVm1`#`dZsa><r9;_;>*fcm7R%s4FrJnsU>73b#& zr>0A_`v!O>Vt3drqM}}(vf+?(K0PehqyHp9A%>ILwI-2^AN}VHTQEQSjJtGqk`~5< z#iig`vHo4J&?Hh<MNF(tTH~gx>Wuqae{@pz+RY*bIx^?R*OdvZ#UhmN`lFe#RfEso zSqA-4Xjco(uzJ&>z%nmJ_L=3p;#Vu%;msD5Ptk?ckJkj4oL;*bm(%JiK^REpvZ^6R z_;F|A%?_eIqqPS~=x<Nk&tG(*8}IcU^5YV0rA@ylS6SoQ_jhLUtJnOAXdR2p3r4?_ zNs5xS3@|kp_t;{uI1TdR9vbmFv`5#yZoHr9lRuH{|G{X@>#5`Y*?Wf5kLrX!?KNhP zTK4r@5=quNipMDLktCAfBn&YfL@8%99(uB_jw*#`iTaU?xSvoxP9gf(m5xi8v{TL< zpKD8gjFduXq;gWm<Vx0;b60N-oD(}VPo0!Q(r@J5pM9qPt<Jo>PA~nPNN<a|;AtW| z1(m%Pz8xb|^4jWp9CA4!;<M`}N1=Fo^m=P(vC_^j3r5pkMu}6S-tWD<?_Dm+mwb>{ z$spBh8ZK16$ss%Jm0c}I3PBRO=ghw1#Xs|BI7f$4J*T2|X6<&vhUYFhrJEa@nYoj? ztj{GJwB#YRx(QAxV<{ai^3$SrgV(<s1sju&QvL5w0K^x2IT2&{RQ;S<iWtH6$)!e4 zs~xspbOpV#Nt4MyFgLDk8y859y8^YRJPBYH7!=S#liS9;eOnJ;x?Tg`q$KQ<0yiHh z$V)-{e)A>|6Vt+m$yj+gZ(`7v<aqAdY4l3@--1%FGod&jKnZRZ%U9>4P?*2XEh|T{ zcSki^p9Lx2d9>Vl?r-2u*J9=VW}038R|?=h;4hQR?|aPYiLGqbvQbHlaNNrjIk6M< z*Us`w(?Z|h7uxtoBa)@RG$%JVH@h#^R{r;og!LgbM9X%6WbVBg?{%EAKRTd$f3)>> zWa`Lwx34x~iPru9Y3<tIp}_aB5nYFlTt-H!r?h!WXj&bEVx^^oB5B<(NlJwnMrH=5 zlu)@u<2H5L*ifP<5mJa5=BbE6siC1kWOl?F=bd)<oPXf>eV%#d>wEcp-uLsqZ{X6* z9#dJ?oX+0yLcGm?;%YADKeCR0j?8lGDI1zhPA<rqvtDb=>kSus?Vh=D_**pdqpqE} zfuGUIJIZbC>FE&&1mqgVK1VmVqs-Sg7_YjzG_|!wzLfo=PGMnT5fKsL;o%s*#gMDq z3Qf)Rl}gA4q{W=Mh#_F>ag&B214+pDFus;>^A)Vo(V_Fa+Lt8i67#vpvfGjtr2^(} z1H2gX?sJ3_09huUZrtnaoDTlbjMpsF2OGC54SS7@j7-?<Z{nMsc6D<4n5d|CEbq-N zI#Hz#QAwQccd7ZgvMd;Bt6!x%?3fnp(s7X#dU;+BM+;|yINj{(CyyU`gq*nAG%-Xw zE@Mh6U%n&=&km+Aga_rQLhs}aliYXJSwWP0{}f`_1x=|;Fe7ch6eeg^fiZi6@0Uqz z1kV`p{;;qX$TKz6)iaPwXr%xlP-824!uh?J{sAlarr3>tz{LgD)eu@(c7%k4KzD(7 zLgUZ$it01<Gb1CVoAbQaZrw^+l5|;33WnuI>9Em`T7LQ2EFU(e>CrJQPXcjTa^WVf zFGqm``~%Hij9r%HuLD7=YS7IVtBqn{-Lyu#&VE^*d(Ek#7l6VH+#mcyRh4q7a-M9l zJJN(ha+BRHo!b?J2LCQ{R}l7ZXr)q3b3HSjx3u^s!5k{m(lS;QvX~G2j%W=zv0r?q zK5cCC14x2J?;vxlCN5mq%;f^uhk1}re9t+RHS4dW=MWVrse2zFByr@~1rnv&s2Ou) zu$S@$vIQzEFgRF@AtQ+-Ay4lioHo$*?~MdAtH!wA_r=>N+Sbm_9Yzerr(r*Fu(ZAn zBTSY-)2-Hsn$amtNl|Y5IibMB{UKev^m3H}i*dt%Grcb?tlX|G_YL!7^48m~Y-)HA zonCe4&K<ik**O!xevSX+)RQYx0`(97c-kv#5@FhF_l42QO3D1d#-FCdz_dVa!ZtQF zw}wzvRP-73(yy-m{sQh*O0dS>7({e{IF*m9_wq#O5c0<vRgvS@&9>~9n?`#f+79C& zB+k%i65xa`(2bPqu<7`*VA!Cns^f`bKMCRjVXZzf(Av%Uf?bkVN-=ZS*VnhRsT;rs zX6J%k6rE2@4DC&kpX3~Hb0Zh{TUlA5m^pMv*N~2-|07O5>cJ_Gm{~MHQ^l;L-Me-H z?TLJz?w0pLv}zvz^k&1%#*I<UdbKcQ#>38Du;Oj;y9$lWbi?}roqIY^KlsF(l)HNm z&P#YmgHYjN-$sY)n>KA4rB6BLiIdY<p~1l=YFU)SZ2-W!pC@SMo8arKu2_Kpmu}MH zY~UlKA>Tt$=<_a<swfoYXzwg$v$dp4$;L?`ZG%M4ay=Y%y+)4s$r1y@v$3x@l!ks_ z#HhdPTA?l|6_r%>$!DU?5yCkLwoc#vwMbQqOjfe+`-E_l>d=8iccuFW<&>D^A%ooI z-w4;`Csh;eWA**T6!ME1mWq&@2aIm2LADX%Jy{c|-{59%;&4@SC5q&#s%a#y#L`U0 zf%p;MOX1zhJ)tN1{JFIalj3dDeK&~~OhAcUu`CnqR{<4Voo(87&jHtMeRo=QO%^Pu zf_|Y>^A~~ZDHz5zdU3l{vm)Syy^)@SGR&7Sw*0uf{40p}-zO&GO2a}!`xrC9dZhq9 zk1Tn<)K+r;_@3{oHA^MFR-hcc+aS|jA<*$rO~ZIVL~w90mU7UA2scTbmFyCdg%hiv zw^7$$L?jaZb1tN$jE;^5e3?P#5w&Q_caG`ns~L}lxm8i|J8(Nq1-7MsR(7nu@upE( z9M#F<<*dHDehEBfj-Ixb)~%&WVvhA<SR9P$Y;mk-Eu-f9#b9Z7HZ3)+Tc&bo-d#{D z=|c#0{xVe3esf5^x!PG)FCwYg+t{3>(-&KWby^9aK0tZe1v7`TtJ}#&cgS$A(ETQ- z;yN}u<WuJ1=qiK!4IWR$Ma_V%yBeEq1>63+jjLa6#^gX}r%S*PAh{ps_mWor`MAxa zky1*#hM7z^;bMy>ufhV~1+4k|m+K82CnIz*DM}V#P*~{g55R*`qz;A~uH}N;5j-9b zN)GVJpSr+I8&Y*l8P#QF6Cek0RqO$-dTPq><8kOjq1-SAl)H2V^(n)q3b_VwpArpv zv{2ZPp0Zbial^1x=?rjE;Qb^JuIlQ~I5zzd4H<fF8&RH6Gx}<4Z*MH=G4z$LA={6o ze{f#R&s!zOPU{U&F{HT7<Ku`5fIGM0$gc7jtbw#5CmR6Edn-=m3ZNM@VyK2+Kg1;5 zx?K5tc#wGXmi7fJPg_`<Ygl|DsX3DJSBK8NWe`%$Etukt1INBxGXhH-^?E3EnQVz> z^|x3^Vz0dIiqSdP)$Zgh7%M6LFlb8a8FxLg)4;EyX?pM;O`lj<mNYTvB~ADcU0f^e zcwHiK-jo=%FlnEc*Ge+^=;%BTPtUfTA52Xjm6yM?HkJK^NC_sXayVrDZm-yN!8WVc zacTNS<2N-`-@`AxIqOL2ticlAGUbKYdRq#jLTqi_+_JyPx1_H%=?}<O<b&>LI7bjd zc8j>QuRd=&I^aI%uROGXbC)~W_bj?YM&k|Nxfx4e=F{Da%4M8198*D>Y~Z&uy<DB6 zN+bh^Gk&(s-%&ViMc|+O3X0UnY25+5B0xZ~hK90?9C>+pF!E+_Jwuy?XTY3Fm6>7H zeFoD*7yz-5sB%stG5Op%xx@7z5XrgkL2)rRpw}o2ObvGF&7H&76R}6o3kQ6g7~mT_ zJwiBwMf>;p5ZD$+jPwNuyyv#VtL`V;jZIIVdgnaF%*;UqBv@N3ZcFRK^faD^tsPGD zg$xDN?ZV-z@R^9_$JL+YzpmgfzooM>Vq5LKVMg>L&zAl5+A#c5Qc}7YQY?S(hlMPS z7vX#`&)hZ1knIY-M6TI(W<5u8^nmPDRSkMC_a-3O)5F69yGK!1y??>x?cJB8ZMV4R zuN|JPC}_IrURZQzOek`uyO>|Y18xbXCVV<A9`|ljV`CXX7h_S>jW2^voM0v;l*s7Q zT>~6~Vqo?&KJf@^qPNp!=L0sIjTl^+pIvY59H(ga&_S_aA4_M_cUc}%b2BnJq7Rwn zuLggIr2;oN9d%t+L~cF8A^f7SlTu;6<%6}u#oN&?=IZEzWZ&>>XpvMK<zJ6%s1sZB zH|OJ!Xa$x2*Qn7Ag{aoC|5b>8bx(g6vVY-s|M!oI6Xa%@4nDQ%aZhiw%i+(~+J47> E0O-3(mH+?% literal 0 HcmV?d00001 From cb1941a6b93f4dada12b04c8ec14b7e47c045efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Wed, 22 Nov 2023 08:08:15 +0100 Subject: [PATCH 20/72] [no ci] [doc] Fix broken links --- docs/Datagrid.md | 2 +- docs/NextJs.md | 2 +- docs/PrevNextButtons.md | 4 ++-- docs/Validation.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Datagrid.md b/docs/Datagrid.md index 247f98b5683..60b6e0c3726 100644 --- a/docs/Datagrid.md +++ b/docs/Datagrid.md @@ -5,7 +5,7 @@ title: "The Datagrid Component" # `<Datagrid>` -The `<Datagrid>` component renders a list of records as a table. It supports sorting, row selection for bulk actions, and an expand panel. It is usually used as a descendant of the [`<List>`](List.md#list) and [`<ReferenceManyField>`](./ReferenceManyField.md) components. Outside these components, it must be used inside a `ListContext`. +The `<Datagrid>` component renders a list of records as a table. It supports sorting, row selection for bulk actions, and an expand panel. It is usually used as a descendant of the [`<List>`](./List.md#list) and [`<ReferenceManyField>`](./ReferenceManyField.md) components. Outside these components, it must be used inside a `ListContext`. <video controls autoplay playsinline muted loop> <source src="./img/Datagrid.mp4" type="video/mp4"/> diff --git a/docs/NextJs.md b/docs/NextJs.md index 55b10172287..27164a992e1 100644 --- a/docs/NextJs.md +++ b/docs/NextJs.md @@ -31,7 +31,7 @@ This creates a project with the following folder structure: | Pages Router | App Router | |-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| -| ![ Next Admin folder structure with Pages Router ]( ./img/next-admin-with-page-router-folder-structure.png ) | ![ Next Admin folder structure with App Router ]( ./img/next-admin-with-app-router-folder-structure.png ) | +| ![Next Admin folder structure with Pages Router](./img/next-admin-with-page-router-folder-structure.png) | ![Next Admin folder structure with App Router](./img/next-admin-with-app-router-folder-structure.png) | ## Adding React-Admin Dependencies diff --git a/docs/PrevNextButtons.md b/docs/PrevNextButtons.md index 294180dbf95..9204948937c 100644 --- a/docs/PrevNextButtons.md +++ b/docs/PrevNextButtons.md @@ -369,8 +369,8 @@ Let's say users want to edit customer records and to navigate between records in - when they navigate to another record, the form is not saved. Thanks to React-admin components, you can solve these issues by using -- [`redirect` prop from `<Edit>`](Edit.md#redirect) with which you can specify the redirect to apply. Here we will choose to stay on the page rather than being redirected to the list view. -- [`warnWhenUnsavedChanges` from `Form`](Form.md#warnwhenunsavedchanges) that will trigger an alert if the user tries to change page while the record has not been saved. +- [`redirect` prop from `<Edit>`](./Edit.md#redirect) with which you can specify the redirect to apply. Here we will choose to stay on the page rather than being redirected to the list view. +- [`warnWhenUnsavedChanges` from `Form`](./Form.md#warnwhenunsavedchanges) that will trigger an alert if the user tries to change page while the record has not been saved. {% raw %} ```tsx diff --git a/docs/Validation.md b/docs/Validation.md index 6c03c0114a7..f51b2fc39b7 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -235,7 +235,7 @@ export default { } ``` -See the [Translation documentation](TranslationTranslating.md#translating-form-validation-errors) for details. +See the [Translation documentation](./TranslationTranslating.md#translating-form-validation-errors) for details. **Tip**: Make sure to define validation functions or array of functions in a variable outside your component, instead of defining them directly in JSX. This can result in a new function or array at every render, and trigger infinite rerender. From 24ac4aff972bec56ced23475c8c7db26c8da3582 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Thu, 23 Nov 2023 14:02:13 +0100 Subject: [PATCH 21/72] [Doc] Update authProvider and dataProvider lists to target the documentation instead of the repository's root --- docs/AuthProviderList.md | 10 +++++----- docs/DataProviderList.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/AuthProviderList.md b/docs/AuthProviderList.md index 97c743c155e..391476e9a03 100644 --- a/docs/AuthProviderList.md +++ b/docs/AuthProviderList.md @@ -7,17 +7,17 @@ title: "Supported Auth Provider Backends" It's very common that your auth logic is so specific that you'll need to write your own `authProvider`. However, the community has built a few open-source Auth Providers that may fit your need: -- **[Auth0](https://auth0.com/)**: [marmelab/ra-auth-auth0](https://github.com/marmelab/ra-auth-auth0) +- **[Auth0](https://auth0.com/)**: [marmelab/ra-auth-auth0](https://github.com/marmelab/ra-auth-auth0/blob/main/packages/ra-auth-auth0/Readme.md) - **[AWS Amplify](https://docs.amplify.aws)**: [MrHertal/react-admin-amplify](https://github.com/MrHertal/react-admin-amplify) - **[AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/setting-up-the-javascript-sdk.html)**: [marmelab/ra-auth-cognito](https://github.com/marmelab/ra-auth-cognito/blob/main/packages/ra-auth-cognito/Readme.md) -- **[Azure Active Directory (using MSAL)](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser)**: [marmelab/ra-auth-msal](https://github.com/marmelab/ra-auth-msal) ([Tutorial](https://marmelab.com/blog/2023/09/13/active-directory-integration-tutorial.html)) +- **[Azure Active Directory (using MSAL)](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser)**: [marmelab/ra-auth-msal](https://github.com/marmelab/ra-auth-msal/blob/main/packages/ra-auth-msal/Readme.md) ([Tutorial](https://marmelab.com/blog/2023/09/13/active-directory-integration-tutorial.html)) - **[Casdoor](https://casdoor.com/)**: [NMB-Lab/reactadmin-casdoor-authprovider](https://github.com/NMB-Lab/reactadmin-casdoor-authprovider) -- **[Directus](https://directus.io/)**: [marmelab/ra-directus](https://github.com/marmelab/ra-directus) +- **[Directus](https://directus.io/)**: [marmelab/ra-directus](https://github.com/marmelab/ra-directus/blob/main/packages/ra-directus/Readme.md) - **[Firebase Auth (Google, Facebook, GitHub, etc.)](https://firebase.google.com/docs/auth/web/firebaseui)**: [benwinding/react-admin-firebase](https://github.com/benwinding/react-admin-firebase#auth-provider) - **[Google Identity & Google Workspace](https://developers.google.com/identity/gsi/web/guides/overview)**: [marmelab/ra-auth-google](https://github.com/marmelab/ra-auth-google/blob/main/packages/ra-auth-google/Readme.md) -- **[Keycloak](https://www.keycloak.org/)**: [marmelab/ra-keycloak](https://github.com/marmelab/ra-keycloak) +- **[Keycloak](https://www.keycloak.org/)**: [marmelab/ra-keycloak](https://github.com/marmelab/ra-keycloak/blob/main/packages/ra-keycloak/Readme.md) - **[Postgrest](https://postgrest.org/)**: [raphiniert-com/ra-data-postgrest](https://github.com/raphiniert-com/ra-data-postgrest) -- **[Supabase](https://supabase.io/)**: [marmelab/ra-supabase](https://github.com/marmelab/ra-supabase) +- **[Supabase](https://supabase.io/)**: [marmelab/ra-supabase](https://github.com/marmelab/ra-supabase/blob/main/packages/ra-supabase/README.md) - **[SurrealDB](https://surrealdb.com/)**: [djedi23/ra-surrealdb](https://github.com/djedi23/ra-surrealdb) Beyond ready-to-use providers, you may find help in these third-party tutorials about integrating more authentication backends: diff --git a/docs/DataProviderList.md b/docs/DataProviderList.md index 9a29e62b2e0..1305b6c3446 100644 --- a/docs/DataProviderList.md +++ b/docs/DataProviderList.md @@ -14,7 +14,7 @@ If you can't find a Data Provider for your backend below, no worries! [Writing a * **[Blitz-js](https://blitzjs.com/docs)**: [theapexlab/ra-data-blitz](https://github.com/theapexlab/ra-data-blitz) * **[Configurable Identity Property REST Client](https://github.com/zachrybaker/ra-data-rest-client)**: [zachrybaker/ra-data-rest-client](https://github.com/zachrybaker/ra-data-rest-client) * **[coreBOS](https://corebos.com/)**: [React-Admin coreBOS Integration](https://github.com/coreBOS/reactadminportal) -* **[Directus](https://directus.io/)**: [marmelab/ra-directus](https://github.com/marmelab/ra-directus) +* **[Directus](https://directus.io/)**: [marmelab/ra-directus](https://github.com/marmelab/ra-directus/blob/main/packages/ra-directus/Readme.md) * **[Django Rest Framework](https://www.django-rest-framework.org/)**: [bmihelac/ra-data-django-rest-framework](https://github.com/bmihelac/ra-data-django-rest-framework) * **[Eve](https://docs.python-eve.org/en/stable/)**: [smeng9/ra-data-eve](https://github.com/smeng9/ra-data-eve) * **[Express & Mongoose](https://github.com/NathanAdhitya/express-mongoose-ra-json-server)**: [NathanAdhitya/express-mongoose-ra-json-server](https://github.com/NathanAdhitya/express-mongoose-ra-json-server) @@ -59,7 +59,7 @@ If you can't find a Data Provider for your backend below, no worries! [Writing a * **[Spring Boot](https://spring.io/projects/spring-boot)**: [vishpat/ra-data-springboot-rest](https://github.com/vishpat/ra-data-springboot-rest) * **[Strapi v3/v4](https://strapi.io/)**: [nazirov91/ra-strapi-rest](https://github.com/nazirov91/ra-strapi-rest) * **[Strapi v4](https://strapi.io/)**: [garridorafa/ra-strapi-v4-rest](https://github.com/garridorafa/ra-strapi-v4-rest) -* **[Supabase](https://supabase.io/)**: [marmelab/ra-supabase](https://github.com/marmelab/ra-supabase) +* **[Supabase](https://supabase.io/)**: [marmelab/ra-supabase](https://github.com/marmelab/ra-supabase/blob/main/packages/ra-supabase/README.md) - **[SurrealDB](https://surrealdb.com/)**: [djedi23/ra-surrealdb](https://github.com/djedi23/ra-surrealdb) * **[TreeQL / PHP-CRUD-API](https://treeql.org/)**: [nkappler/ra-data-treeql](https://github.com/nkappler/ra-data-treeql) * **[WooCommerce REST API](https://woocommerce.github.io/woocommerce-rest-api-docs)**: [zackha/ra-data-woocommerce](https://github.com/zackha/ra-data-woocommerce) From ffc685bc9b8d6261e7fcaa0b8fda721651c001c3 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 24 Nov 2023 10:31:55 +0100 Subject: [PATCH 22/72] [no ci] report changes from 'persist column order/size in store' --- docs/DatagridAG.md | 86 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/docs/DatagridAG.md b/docs/DatagridAG.md index f65623e75fa..5c9bf96cbf2 100644 --- a/docs/DatagridAG.md +++ b/docs/DatagridAG.md @@ -90,16 +90,17 @@ Here are the important things to note: ## Props -| Prop | Required | Type | Default | Description | -| --- | --- | --- | --- | --- | -| `columnDefs` | Required | Array | n/a | The columns definitions | -| `bulkActionButtons` | Optional | Element | `<BulkDelete Button>` | The component used to render the bulk action buttons | -| `cellRenderer` | Optional | String, Function or Element | | Allows to use a custom component to render the cell content | -| `defaultColDef` | Optional | Object | | The default column definition (applied to all columns) | -| `mutationOptions` | Optional | Object | | The mutation options | -| `theme` | Optional | String | `'ag-theme-alpine'` | The name of the ag-grid theme | -| `pagination` | Optional | Boolean | `true` | Enable or disable pagination | -| `sx` | Optional | Object | | The sx prop passed down to the wrapping `<div>` element | +| Prop | Required | Type | Default | Description | +| ------------------- | -------- | --------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------- | +| `columnDefs` | Required | Array | n/a | The columns definitions | +| `bulkActionButtons` | Optional | Element | `<BulkDelete Button>` | The component used to render the bulk action buttons | +| `cellRenderer` | Optional | String, Function or Element | | Allows to use a custom component to render the cell content | +| `defaultColDef` | Optional | Object | | The default column definition (applied to all columns) | +| `mutationOptions` | Optional | Object | | The mutation options | +| `preferenceKey` | Optional | String or `false` | `${resource}.ag-grid.params` | The key used to persist columns order and sizing in the Store. `false` disables persistence. | +| `sx` | Optional | Object | | The sx prop passed down to the wrapping `<div>` element | +| `theme` | Optional | String | `'ag-theme-alpine'` | The name of the ag-grid theme | +| `pagination` | Optional | Boolean | `true` | Enable or disable pagination | `<DatagridAG>` also accepts the same props as [`<AgGridReact>`](https://www.ag-grid.com/react-data-grid/grid-options/) with the exception of `rowData`, since the data is fetched from the List context. @@ -336,6 +337,49 @@ export const PostList = () => { {% endraw %} +This also allows to display a notification after the mutation succeeds. + +{% raw %} + +```tsx +import 'ag-grid-community/styles/ag-grid.css'; +import 'ag-grid-community/styles/ag-theme-alpine.css'; +import React from 'react'; +import { List, useNotify } from 'react-admin'; +import { DatagridAG } from '@react-admin/ra-editable-datagrid'; + +export const PostList = () => { + const columnDefs = [ + { field: 'title' }, + { field: 'published_at' }, + { field: 'body' }, + ]; + const notify = useNotify(); + const onSuccess = React.useCallback(() => { + notify('ra.notification.updated', { + type: 'info', + messageArgs: { + smart_count: 1, + }, + undoable: true, + }); + }, [notify]); + return ( + <List perPage={10000} pagination={false}> + <DatagridAG + columnDefs={columnDefs} + mutationOptions={{ + mutationMode: 'undoable', + onSuccess, + }} + /> + </List> + ); +}; +``` + +{% endraw %} + ## `theme` You can use a different theme for the grid by passing a `theme` prop. You can for instance use one of the [themes provided by ag-grid](https://www.ag-grid.com/react-data-grid/themes/), like `ag-theme-balham` or `ag-theme-alpine-dark`: @@ -405,6 +449,28 @@ const CarList = () => { Your browser does not support the video tag. </video> +### `preferenceKey` + +`<DatagridAG>` will store the order and size of columns in the [Store](./Store.md), under the key `${resource}.ag-grid.params`. + +If you wish to change the key used to store the columns order and size, you can pass a `preferenceKey` prop to `<DatagridAG>`. + +```tsx +<List perPage={10000} pagination={false}> + <DatagridAG columnDefs={columnDefs} preferenceKey="my-post-list" /> +</List> +``` + +If, instead, you want to disable the persistence of the columns order and size, you can pass `false` to the `preferenceKey` prop: + +```tsx +<List perPage={10000} pagination={false}> + <DatagridAG columnDefs={columnDefs} preferenceKey={false} /> +</List> +``` + +**Note:** Saving the columns size in the Store is disabled when using the [`flex` config](https://www.ag-grid.com/react-data-grid/column-sizing/#column-flex). + ## `sx` You can also use [the `sx` prop](./SX.md) to customize the grid's style: From b967662eba19556c05f6b3a4e11fad227142fe2f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 24 Nov 2023 10:34:03 +0100 Subject: [PATCH 23/72] [no ci] Replace package to ra-datagrid-ag --- docs/DatagridAG.md | 52 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/DatagridAG.md b/docs/DatagridAG.md index 5c9bf96cbf2..080ccab0f2f 100644 --- a/docs/DatagridAG.md +++ b/docs/DatagridAG.md @@ -44,15 +44,15 @@ Additionally, `<DatagridAG>` is compatible with the [Enterprise version of ag-gr ## Usage -First, install the `@react-admin/ra-editable-datagrid` package: +First, install the `@react-admin/ra-datagrid-ag` package: ```sh -npm install --save @react-admin/ra-editable-datagrid +npm install --save @react-admin/ra-datagrid-ag # or -yarn add @react-admin/ra-editable-datagrid +yarn add @react-admin/ra-datagrid-ag ``` -**Tip**: `ra-editable-datagrid` is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. +**Tip**: `ra-datagrid-ag` is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. Then, use `<DatagridAG>` as a child of a react-admin `<List>`, `<ReferenceManyField>`, or any other component that creates a `ListContext`. @@ -61,7 +61,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -142,7 +142,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const truncate = (str: string, n: number) => { return str.length > n ? str.slice(0, n - 1) + '...' : str; @@ -191,7 +191,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -221,7 +221,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { EmailField, List, ReferenceField, TextField } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const CommentList = () => { const columnDefs = [ @@ -267,7 +267,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List, BulkExportButton, BulkDeleteButton } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const PostBulkActionButtons = () => ( <> @@ -313,7 +313,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -346,7 +346,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List, useNotify } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -389,7 +389,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -420,7 +420,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const CarList = () => { const columnDefs = [ @@ -482,7 +482,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -514,7 +514,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -547,7 +547,7 @@ import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { AgGridReact } from 'ag-grid-react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -584,7 +584,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -613,7 +613,7 @@ import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { AgGridReact } from 'ag-grid-react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -664,7 +664,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -696,7 +696,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List, BulkExportButton, BulkDeleteButton } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const PostBulkActionButtons = () => ( <> @@ -746,7 +746,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const CarList = () => { const columnDefs = [ @@ -778,7 +778,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React, { useMemo } from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; import 'ag-grid-enterprise'; const CarList = () => { @@ -820,7 +820,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -845,7 +845,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -868,7 +868,7 @@ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; export const PostList = () => { const columnDefs = [ @@ -905,7 +905,7 @@ import { AgGridReact } from 'ag-grid-react'; import 'ag-grid-enterprise'; import React from 'react'; import { List } from 'react-admin'; -import { DatagridAG } from '@react-admin/ra-editable-datagrid'; +import { DatagridAG } from '@react-admin/ra-datagrid-ag'; const OlympicWinnersList = () => { const columnDefs = [ From 2c9e1d06cf422c687a2bc65ff4b43ebc4d9f5d33 Mon Sep 17 00:00:00 2001 From: Ross <56581954+rossb220@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:20:25 +0000 Subject: [PATCH 24/72] docs: inspector component details for configuration feature (#9458) * docs: inspector component details for configuration feature * Update docs/Configurable.md Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> * Update docs/Configurable.md Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> * Update docs/Configurable.md Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> * Update docs/WrapperField.md Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> * Update docs/WrapperField.md Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> --------- Co-authored-by: Jean-Baptiste Kaiser <jbaptiste.kaiser@gmail.com> --- docs/Configurable.md | 50 ++++++++++++++++++++++++++++++++++++++++++++ docs/WrapperField.md | 22 ++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/Configurable.md b/docs/Configurable.md index 37cc53e2ede..3d35b20743c 100644 --- a/docs/Configurable.md +++ b/docs/Configurable.md @@ -214,3 +214,53 @@ const MyAppBar = () => ( </AppBar> ); ``` + +## `<Inspector>` + +The `<Inspector>` is already included in the layouts provided by react-admin. If you are using a custom layout, you need to add the `<Inspector>` component to your layout. + +{% raw %} +```jsx +// in src/MyLayout.js +import * as React from 'react'; +import { Box } from '@mui/material'; +import { AppBar, Menu, Sidebar, Inspector } from 'react-admin'; + +const MyLayout = ({ children, dashboard }) => ( + <Box + display="flex" + flexDirection="column" + zIndex={1} + minHeight="100vh" + backgroundColor="theme.palette.background.default" + position="relative" + > + <Box + display="flex" + flexDirection="column" + overflowX="auto" + > + <AppBar /> + <Box display="flex" flexGrow={1}> + <Sidebar> + <Menu hasDashboard={!!dashboard} /> + </Sidebar> + <Box + display="flex" + flexDirection="column" + flexGrow={2} + p={3} + marginTop="4em" + paddingLeft={5} + > + {children} + </Box> + </Box> + </Box> + <Inspector /> + </Box> +); + +export default MyLayout; +``` +{% endraw %} \ No newline at end of file diff --git a/docs/WrapperField.md b/docs/WrapperField.md index a6a66aa3376..f63d6e1e2fd 100644 --- a/docs/WrapperField.md +++ b/docs/WrapperField.md @@ -5,7 +5,7 @@ title: "The WrapperField Component" # `<WrapperField>` -This component simply renders its children. Why would you want to use such a dumb component? To combine several fields in a single cell (in a `<Datagrid>`) or in a single row (in a `<SimpleShowLayout>`). +This component simply renders its children. Why would you want to use such a dumb component? To combine several fields in a single cell (in a `<Datagrid>`), in a single row (in a `<SimpleShowLayout>`) or in a group of inputs (in a `<SimpleFormConfigurable>`) . `<WrapperField>` allows to define the `label` and sort field for a combination of fields: @@ -25,4 +25,24 @@ const BookList = () => ( ); ``` +```jsx +import { Edit, WrapperField, TextInput, SimpleFormConfigurable } from 'react-admin'; +import { Stack } from '@mui/material'; + +const PostEdit = () => ( + <Edit> + <SimpleFormConfigurable> + <TextInput source="title"/> + <WrapperField source="author"> + <Stack> + <TextInput source="author_first_name"/> + <TextInput source="author_last_name"/> + </Stack> + </WrapperField> + </SimpleFormConfigurable> + </Edit> +); +``` + + **Tip**: If you just want to combine two fields in a string, check [the `<FunctionField>` component](./FunctionField.md) instead. \ No newline at end of file From 9d2db58912ccf787888229c7380e956fa7055d7c Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 24 Nov 2023 11:10:23 +0100 Subject: [PATCH 25/72] changelog for v4.16.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e5a52b4b1..d02c4a51b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v4.16.1 + +* Fix `<FileInput>` should display a validation errors right away when form mode is 'onChange' ([#9459](https://github.com/marmelab/react-admin/pull/9459)) ([slax57](https://github.com/slax57)) +* [TypeScript] Fix useRecordContext may return undefined ([#9460](https://github.com/marmelab/react-admin/pull/9460)) ([groomain](https://github.com/groomain)) +* [Doc] Add link to new demo: Note-taking app ([#9465](https://github.com/marmelab/react-admin/pull/9465)) ([fzaninotto](https://github.com/fzaninotto)) +* [Doc] Add headless section in pages components ([#9447](https://github.com/marmelab/react-admin/pull/9447)) ([fzaninotto](https://github.com/fzaninotto)) +* [Doc] Add example showing how to add `<Inspector>` to a custom layout ([#9458](https://github.com/marmelab/react-admin/pull/9458)) ([rossb220](https://github.com/rossb220)) +* [Doc] Update `<DatagridAG>` doc to use the new package, and document the column order/size persistence ([#9472](https://github.com/marmelab/react-admin/pull/9472)) ([slax57](https://github.com/slax57)) +* [Doc] Update authProvider and dataProvider lists to target the documentation instead of the repository's root ([#9471](https://github.com/marmelab/react-admin/pull/9471)) ([slax57](https://github.com/slax57)) +* [Website] Reorder documentation's Discord and Github icons to match the website order ([#9454](https://github.com/marmelab/react-admin/pull/9454)) ([adguernier](https://github.com/adguernier)) + ## v4.16.0 * Add `<SingleFieldList empty gap direction>` props, and allow it to be used without `children` ([#9439](https://github.com/marmelab/react-admin/pull/9439)) ([fzaninotto](https://github.com/fzaninotto)) From 64bb5db5d5a33018223a97352aa664757030b15d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 24 Nov 2023 11:31:46 +0100 Subject: [PATCH 26/72] v4.16.1 --- examples/data-generator/package.json | 4 ++-- examples/simple/package.json | 14 +++++++------- lerna.json | 2 +- packages/create-react-admin/package.json | 2 +- packages/ra-core/package.json | 2 +- packages/ra-data-fakerest/package.json | 4 ++-- packages/ra-data-graphql-simple/package.json | 4 ++-- packages/ra-data-graphql/package.json | 2 +- packages/ra-data-json-server/package.json | 4 ++-- packages/ra-data-localforage/package.json | 4 ++-- packages/ra-data-localstorage/package.json | 4 ++-- packages/ra-data-simple-rest/package.json | 4 ++-- packages/ra-i18n-i18next/package.json | 4 ++-- packages/ra-i18n-polyglot/package.json | 4 ++-- packages/ra-input-rich-text/package.json | 10 +++++----- packages/ra-language-english/package.json | 4 ++-- packages/ra-language-french/package.json | 4 ++-- packages/ra-no-code/package.json | 6 +++--- packages/ra-ui-materialui/package.json | 8 ++++---- packages/react-admin/package.json | 10 +++++----- 20 files changed, 50 insertions(+), 50 deletions(-) diff --git a/examples/data-generator/package.json b/examples/data-generator/package.json index 332f6884de1..1170fe1115c 100644 --- a/examples/data-generator/package.json +++ b/examples/data-generator/package.json @@ -1,6 +1,6 @@ { "name": "data-generator-retail", - "version": "4.16.0", + "version": "4.16.1", "homepage": "https://github.com/marmelab/react-admin/tree/master/examples/data-generator", "bugs": "https://github.com/marmelab/react-admin/issues", "license": "MIT", @@ -19,7 +19,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.16.0", + "ra-core": "^4.16.1", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/examples/simple/package.json b/examples/simple/package.json index d176cfbc8a1..ee86922d017 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -1,6 +1,6 @@ { "name": "simple", - "version": "4.16.0", + "version": "4.16.1", "private": true, "scripts": { "dev": "vite", @@ -17,13 +17,13 @@ "lodash": "~4.17.5", "prop-types": "^15.7.2", "proxy-polyfill": "^0.3.0", - "ra-data-fakerest": "^4.16.0", - "ra-i18n-polyglot": "^4.16.0", - "ra-input-rich-text": "^4.16.0", - "ra-language-english": "^4.16.0", - "ra-language-french": "^4.16.0", + "ra-data-fakerest": "^4.16.1", + "ra-i18n-polyglot": "^4.16.1", + "ra-input-rich-text": "^4.16.1", + "ra-language-english": "^4.16.1", + "ra-language-french": "^4.16.1", "react": "^17.0.0", - "react-admin": "^4.16.0", + "react-admin": "^4.16.1", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-query": "^3.32.1", diff --git a/lerna.json b/lerna.json index 277d7360c11..e628e515319 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "lerna": "2.5.1", "packages": ["examples/data-generator", "examples/simple", "packages/*"], - "version": "4.16.0" + "version": "4.16.1" } diff --git a/packages/create-react-admin/package.json b/packages/create-react-admin/package.json index 7a37ff1b4c3..52cf72efa8f 100644 --- a/packages/create-react-admin/package.json +++ b/packages/create-react-admin/package.json @@ -1,7 +1,7 @@ { "name": "create-react-admin", "description": "A CLI to quickly start a new react-admin project", - "version": "4.16.0", + "version": "4.16.1", "license": "MIT", "bin": "lib/cli.js", "authors": [ diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index c068d742fe1..c0684d17924 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "4.16.0", + "version": "4.16.1", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", diff --git a/packages/ra-data-fakerest/package.json b/packages/ra-data-fakerest/package.json index aa4f2a3230d..b0b57af6ad3 100644 --- a/packages/ra-data-fakerest/package.json +++ b/packages/ra-data-fakerest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-fakerest", - "version": "4.16.0", + "version": "4.16.1", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -43,7 +43,7 @@ "@types/jest": "^29.5.2", "cross-env": "^5.2.0", "expect": "^27.4.6", - "ra-core": "^4.16.0", + "ra-core": "^4.16.1", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index d152f0d5b2f..423c9200a6f 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql-simple", - "version": "4.16.0", + "version": "4.16.1", "description": "A GraphQL simple data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -37,7 +37,7 @@ "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.17.5", "pluralize": "~7.0.0", - "ra-data-graphql": "^4.16.0" + "ra-data-graphql": "^4.16.1" }, "peerDependencies": { "graphql": "^15.6.0", diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index 08ea50a493b..6edbdecc9c1 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql", - "version": "4.16.0", + "version": "4.16.1", "description": "A GraphQL data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/ra-data-json-server/package.json b/packages/ra-data-json-server/package.json index 390c78597d7..d2556fc3a36 100644 --- a/packages/ra-data-json-server/package.json +++ b/packages/ra-data-json-server/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "4.16.0", + "version": "4.16.1", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "^7.1.1", - "ra-core": "^4.16.0" + "ra-core": "^4.16.1" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localforage/package.json b/packages/ra-data-localforage/package.json index b3f0a37844a..c16b0592375 100644 --- a/packages/ra-data-localforage/package.json +++ b/packages/ra-data-localforage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-forage", - "version": "4.16.0", + "version": "4.16.1", "description": "LocalForage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -42,7 +42,7 @@ "dependencies": { "localforage": "^1.7.1", "lodash": "~4.17.5", - "ra-data-fakerest": "^4.16.0" + "ra-data-fakerest": "^4.16.1" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localstorage/package.json b/packages/ra-data-localstorage/package.json index a90dadef5a6..d62695471f2 100644 --- a/packages/ra-data-localstorage/package.json +++ b/packages/ra-data-localstorage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-storage", - "version": "4.16.0", + "version": "4.16.1", "description": "Local storage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -38,7 +38,7 @@ }, "dependencies": { "lodash": "~4.17.5", - "ra-data-fakerest": "^4.16.0" + "ra-data-fakerest": "^4.16.1" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-simple-rest/package.json b/packages/ra-data-simple-rest/package.json index 94470d459bd..6532924e1fa 100644 --- a/packages/ra-data-simple-rest/package.json +++ b/packages/ra-data-simple-rest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "4.16.0", + "version": "4.16.1", "description": "Simple REST data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -29,7 +29,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.16.0", + "ra-core": "^4.16.1", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json index 9a46ceb9999..451151fd5b0 100644 --- a/packages/ra-i18n-i18next/package.json +++ b/packages/ra-i18n-i18next/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-i18next", - "version": "4.16.0", + "version": "4.16.1", "description": "i18next i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "i18next": "^23.5.1", - "ra-core": "^4.16.0", + "ra-core": "^4.16.1", "react-i18next": "^13.2.2" }, "devDependencies": { diff --git a/packages/ra-i18n-polyglot/package.json b/packages/ra-i18n-polyglot/package.json index 71eb90bdc4e..f784e59f0f7 100644 --- a/packages/ra-i18n-polyglot/package.json +++ b/packages/ra-i18n-polyglot/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-polyglot", - "version": "4.16.0", + "version": "4.16.1", "description": "Polyglot i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "node-polyglot": "^2.2.2", - "ra-core": "^4.16.0" + "ra-core": "^4.16.1" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-input-rich-text/package.json b/packages/ra-input-rich-text/package.json index 97b35476a77..0502c8c8708 100644 --- a/packages/ra-input-rich-text/package.json +++ b/packages/ra-input-rich-text/package.json @@ -1,6 +1,6 @@ { "name": "ra-input-rich-text", - "version": "4.16.0", + "version": "4.16.1", "description": "<RichTextInput> component for react-admin, useful for editing HTML code in admin GUIs.", "author": "Gildas Garcia", "repository": "marmelab/react-admin", @@ -51,10 +51,10 @@ "@testing-library/react": "^11.2.3", "@tiptap/extension-mention": "^2.0.3", "@tiptap/suggestion": "^2.0.3", - "data-generator-retail": "^4.16.0", - "ra-core": "^4.16.0", - "ra-data-fakerest": "^4.16.0", - "ra-ui-materialui": "^4.16.0", + "data-generator-retail": "^4.16.1", + "ra-core": "^4.16.1", + "ra-data-fakerest": "^4.16.1", + "ra-ui-materialui": "^4.16.1", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/ra-language-english/package.json b/packages/ra-language-english/package.json index 21fbe26001c..c3b7156af14 100644 --- a/packages/ra-language-english/package.json +++ b/packages/ra-language-english/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-english", - "version": "4.16.0", + "version": "4.16.1", "description": "English messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.16.0" + "ra-core": "^4.16.1" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-language-french/package.json b/packages/ra-language-french/package.json index bd859a17433..46634d23c44 100644 --- a/packages/ra-language-french/package.json +++ b/packages/ra-language-french/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-french", - "version": "4.16.0", + "version": "4.16.1", "description": "French messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.16.0" + "ra-core": "^4.16.1" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index c1e649d922d..bed913eaf37 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -1,6 +1,6 @@ { "name": "ra-no-code", - "version": "4.16.0", + "version": "4.16.1", "description": "", "files": [ "*.md", @@ -48,8 +48,8 @@ "lodash": "~4.17.5", "papaparse": "^5.3.0", "prop-types": "^15.6.1", - "ra-data-local-storage": "^4.16.0", - "react-admin": "^4.16.0", + "ra-data-local-storage": "^4.16.1", + "react-admin": "^4.16.1", "react-dropzone": "^12.0.4", "react-query": "^3.32.1" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 3ca5ef397f9..b98df63557a 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "4.16.0", + "version": "4.16.1", "description": "UI Components for react-admin with Material UI", "files": [ "*.md", @@ -35,9 +35,9 @@ "file-api": "~0.10.4", "history": "^5.1.0", "ignore-styles": "~5.0.1", - "ra-core": "^4.16.0", - "ra-i18n-polyglot": "^4.16.0", - "ra-language-english": "^4.16.0", + "ra-core": "^4.16.1", + "ra-i18n-polyglot": "^4.16.1", + "ra-language-english": "^4.16.1", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index f1a9b3dce09..4111cf5e109 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "4.16.0", + "version": "4.16.1", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -41,10 +41,10 @@ "@mui/icons-material": "^5.0.1", "@mui/material": "^5.0.2", "history": "^5.1.0", - "ra-core": "^4.16.0", - "ra-i18n-polyglot": "^4.16.0", - "ra-language-english": "^4.16.0", - "ra-ui-materialui": "^4.16.0", + "ra-core": "^4.16.1", + "ra-i18n-polyglot": "^4.16.1", + "ra-language-english": "^4.16.1", + "ra-ui-materialui": "^4.16.1", "react-hook-form": "^7.43.9", "react-router": "^6.1.0", "react-router-dom": "^6.1.0" From 6135c2b1b9c1c678eb4d3ca4297e9a124282d1e1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Fri, 24 Nov 2023 11:32:42 +0100 Subject: [PATCH 27/72] update yarn.lock --- yarn.lock | 82 +++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5c016c48f36..b6c06a7b4d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9720,14 +9720,14 @@ __metadata: languageName: node linkType: hard -"data-generator-retail@^4.12.0, data-generator-retail@^4.16.0, data-generator-retail@workspace:examples/data-generator": +"data-generator-retail@^4.12.0, data-generator-retail@^4.16.1, data-generator-retail@workspace:examples/data-generator": version: 0.0.0-use.local resolution: "data-generator-retail@workspace:examples/data-generator" dependencies: cross-env: ^5.2.0 date-fns: ^2.19.0 faker: ^4.1.0 - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18086,7 +18086,7 @@ __metadata: languageName: node linkType: hard -"ra-core@^4.16.0, ra-core@workspace:packages/ra-core": +"ra-core@^4.16.1, ra-core@workspace:packages/ra-core": version: 0.0.0-use.local resolution: "ra-core@workspace:packages/ra-core" dependencies: @@ -18132,7 +18132,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.16.0, ra-data-fakerest@workspace:packages/ra-data-fakerest": +"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.16.1, ra-data-fakerest@workspace:packages/ra-data-fakerest": version: 0.0.0-use.local resolution: "ra-data-fakerest@workspace:packages/ra-data-fakerest" dependencies: @@ -18140,7 +18140,7 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 fakerest: ^3.0.0 - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18158,7 +18158,7 @@ __metadata: graphql-ast-types-browser: ~1.0.2 lodash: ~4.17.5 pluralize: ~7.0.0 - ra-data-graphql: ^4.16.0 + ra-data-graphql: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18167,7 +18167,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-graphql@^4.12.0, ra-data-graphql@^4.16.0, ra-data-graphql@workspace:packages/ra-data-graphql": +"ra-data-graphql@^4.12.0, ra-data-graphql@^4.16.1, ra-data-graphql@workspace:packages/ra-data-graphql": version: 0.0.0-use.local resolution: "ra-data-graphql@workspace:packages/ra-data-graphql" dependencies: @@ -18190,7 +18190,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18203,7 +18203,7 @@ __metadata: cross-env: ^5.2.0 localforage: ^1.7.1 lodash: ~4.17.5 - ra-data-fakerest: ^4.16.0 + ra-data-fakerest: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18211,13 +18211,13 @@ __metadata: languageName: unknown linkType: soft -"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.16.0, ra-data-local-storage@workspace:packages/ra-data-localstorage": +"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.16.1, ra-data-local-storage@workspace:packages/ra-data-localstorage": version: 0.0.0-use.local resolution: "ra-data-local-storage@workspace:packages/ra-data-localstorage" dependencies: cross-env: ^5.2.0 lodash: ~4.17.5 - ra-data-fakerest: ^4.16.0 + ra-data-fakerest: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18231,7 +18231,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18246,26 +18246,26 @@ __metadata: cross-env: ^5.2.0 i18next: ^23.5.1 i18next-resources-to-backend: ^1.1.4 - ra-core: ^4.16.0 + ra-core: ^4.16.1 react-i18next: ^13.2.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.16.0, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": +"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.16.1, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": version: 0.0.0-use.local resolution: "ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot" dependencies: cross-env: ^5.2.0 node-polyglot: ^2.2.2 - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.16.0, ra-input-rich-text@workspace:packages/ra-input-rich-text": +"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.16.1, ra-input-rich-text@workspace:packages/ra-input-rich-text": version: 0.0.0-use.local resolution: "ra-input-rich-text@workspace:packages/ra-input-rich-text" dependencies: @@ -18287,10 +18287,10 @@ __metadata: "@tiptap/starter-kit": ^2.0.3 "@tiptap/suggestion": ^2.0.3 clsx: ^1.1.1 - data-generator-retail: ^4.16.0 - ra-core: ^4.16.0 - ra-data-fakerest: ^4.16.0 - ra-ui-materialui: ^4.16.0 + data-generator-retail: ^4.16.1 + ra-core: ^4.16.1 + ra-data-fakerest: ^4.16.1 + ra-ui-materialui: ^4.16.1 react: ^17.0.0 react-dom: ^17.0.0 react-hook-form: ^7.43.9 @@ -18307,21 +18307,21 @@ __metadata: languageName: unknown linkType: soft -"ra-language-english@^4.12.0, ra-language-english@^4.16.0, ra-language-english@workspace:packages/ra-language-english": +"ra-language-english@^4.12.0, ra-language-english@^4.16.1, ra-language-english@workspace:packages/ra-language-english": version: 0.0.0-use.local resolution: "ra-language-english@workspace:packages/ra-language-english" dependencies: - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-language-french@^4.12.0, ra-language-french@^4.16.0, ra-language-french@workspace:packages/ra-language-french": +"ra-language-french@^4.12.0, ra-language-french@^4.16.1, ra-language-french@workspace:packages/ra-language-french": version: 0.0.0-use.local resolution: "ra-language-french@workspace:packages/ra-language-french" dependencies: - ra-core: ^4.16.0 + ra-core: ^4.16.1 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18342,9 +18342,9 @@ __metadata: lodash: ~4.17.5 papaparse: ^5.3.0 prop-types: ^15.6.1 - ra-data-local-storage: ^4.16.0 + ra-data-local-storage: ^4.16.1 react: ^17.0.0 - react-admin: ^4.16.0 + react-admin: ^4.16.1 react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 @@ -18358,7 +18358,7 @@ __metadata: languageName: unknown linkType: soft -"ra-ui-materialui@^4.16.0, ra-ui-materialui@workspace:packages/ra-ui-materialui": +"ra-ui-materialui@^4.16.1, ra-ui-materialui@workspace:packages/ra-ui-materialui": version: 0.0.0-use.local resolution: "ra-ui-materialui@workspace:packages/ra-ui-materialui" dependencies: @@ -18381,9 +18381,9 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.0 query-string: ^7.1.1 - ra-core: ^4.16.0 - ra-i18n-polyglot: ^4.16.0 - ra-language-english: ^4.16.0 + ra-core: ^4.16.1 + ra-i18n-polyglot: ^4.16.1 + ra-language-english: ^4.16.1 react: ^17.0.0 react-dom: ^17.0.0 react-dropzone: ^12.0.4 @@ -18561,7 +18561,7 @@ __metadata: languageName: unknown linkType: soft -"react-admin@^4.12.0, react-admin@^4.16.0, react-admin@workspace:packages/react-admin": +"react-admin@^4.12.0, react-admin@^4.16.1, react-admin@workspace:packages/react-admin": version: 0.0.0-use.local resolution: "react-admin@workspace:packages/react-admin" dependencies: @@ -18572,10 +18572,10 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 history: ^5.1.0 - ra-core: ^4.16.0 - ra-i18n-polyglot: ^4.16.0 - ra-language-english: ^4.16.0 - ra-ui-materialui: ^4.16.0 + ra-core: ^4.16.1 + ra-i18n-polyglot: ^4.16.1 + ra-language-english: ^4.16.1 + ra-ui-materialui: ^4.16.1 react-hook-form: ^7.43.9 react-router: ^6.1.0 react-router-dom: ^6.1.0 @@ -20135,13 +20135,13 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.2 proxy-polyfill: ^0.3.0 - ra-data-fakerest: ^4.16.0 - ra-i18n-polyglot: ^4.16.0 - ra-input-rich-text: ^4.16.0 - ra-language-english: ^4.16.0 - ra-language-french: ^4.16.0 + ra-data-fakerest: ^4.16.1 + ra-i18n-polyglot: ^4.16.1 + ra-input-rich-text: ^4.16.1 + ra-language-english: ^4.16.1 + ra-language-french: ^4.16.1 react: ^17.0.0 - react-admin: ^4.16.0 + react-admin: ^4.16.1 react-app-polyfill: ^1.0.4 react-dom: ^17.0.0 react-hook-form: ^7.43.9 From a2383891679f6b1496794ed24f9a8131731c54d8 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Fri, 24 Nov 2023 17:57:56 +0100 Subject: [PATCH 28/72] Fix `<Pagination>` logs a warning Material-ui v5.15.18 introduced a new prop injected to `PaginationAction`, `slotProps`, which isn't supposed to be passed down to the `Pagination` component. Refs https://github.com/mui/material-ui/pull/39353 Closes `<Pagination>` logs a warning #9470 --- .../ra-ui-materialui/src/list/pagination/PaginationActions.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx b/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx index 851c5f4048b..f87fadc8fd8 100644 --- a/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/PaginationActions.tsx @@ -92,5 +92,6 @@ const Root = styled('div', { const sanitizeRestProps = ({ nextIconButtonProps, backIconButtonProps, + slotProps, ...rest }: any) => rest; From 7601a23bbe9305c68aef44e9e0ecb1e83369e30d Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Tue, 28 Nov 2023 14:37:10 +0100 Subject: [PATCH 29/72] [Doc] Add link to tutorial for headless admin --- docs/AdvancedTutorials.md | 112 +++++++++----------------------------- docs/Features.md | 7 ++- 2 files changed, 33 insertions(+), 86 deletions(-) diff --git a/docs/AdvancedTutorials.md b/docs/AdvancedTutorials.md index 9ed41fa248f..83c142d1dd4 100644 --- a/docs/AdvancedTutorials.md +++ b/docs/AdvancedTutorials.md @@ -5,88 +5,30 @@ title: "Advanced Tutorials" # Advanced Tutorials -## Creating a Record Related to the Current One - -This tutorial explains how to add a button on a show or edit page to create a new resource related to the one displayed. - -<video controls autoplay playsinline muted loop> - <source src="https://marmelab.com/5b5e4e19163f7baca76cce020c23c41a/create-related.mp4" type="video/mp4" /> - Your browser does not support the video tag. -</video> - -* [Article](https://marmelab.com/blog/2023/10/12/react-admin-v4-advanced-recipes-creating-a-record-related-to-the-current-one.html) -* [Code](https://github.com/marmelab/react-admin/blob/master/examples/demo/src/products/ProductEdit.tsx) - -## Custom Forms and UI for related records - -This tutorial explains how to have a `create`, `show` or `edit` view of a referenced resource inside a modal or a sliding side panel. - -![Screencast](https://marmelab.com/dd58004986d3bb98a32972ba8fd25fc8/screencast.gif) - -* [Article](https://marmelab.com/blog/2020/04/27/react-admin-tutorials-custom-forms-related-records.html) -* [Codesandbox](https://codesandbox.io/s/react-admin-v3-advanced-recipes-quick-createpreview-voyci) - -## Build a Timeline as a replacement for a List component - -This tutorial shows how to use pure react to implement a custom component replacing react-admin default List. - -![Screencast](https://marmelab.com/d9b4cf0e7faf3ed208c102f8b2334409/storybook_App5.gif) - -* [Article](https://marmelab.com/blog/2019/01/17/react-timeline.html) -* [Repository](https://github.com/marmelab/timeline-react-admin) - -## Creating and Editing a Record From the List Page - -This tutorial shows how to display Creation and Edition forms in a drawer or a dialog from the List page. - -![Screencast](https://marmelab.com/07b25da5494055c4306dd7e7a48fd010/end-result.gif) - -* [Article](https://marmelab.com/blog/2019/02/07/react-admin-advanced-recipes-creating-and-editing-a-record-from-the-list-page.html) -* [Codesandbox](https://codesandbox.io/s/lrm6kl00nl) - -## Add a User Profile Page - -This tutorial explains how to create a profile page based on an `<Edit>` component, and accessible as a standalone page. - -![Screencast](https://marmelab.com/668056e9d8273ff5ce75dfc641151a90/end_result.gif) - -* [Article](https://marmelab.com/blog/2019/03/07/react-admin-advanced-recipes-user-profile.html) -* [Codesandbox](https://codesandbox.io/s/o1jmj4lwv9) - -## Supplying your own Defaults to React Admin - -This article shows how you can customize many parts of the framework without repeating yourself. - -![Screencast](https://marmelab.com/54d42faced9043f7933df212cbda0f1b/react-admin-edit-defaults.gif) - -* [Article](https://marmelab.com/blog/2019/03/27/supplying-your-own-defaults-to-react-admin.html) -* [Codesandbox](https://codesandbox.io/s/qzxx4mjl59) - -## OpenID Connect Authentication with React Admin - -![OpenID Connect on React Admin with a button "Login With Google"](./img/openid-connect-example.png) - -* [Live Example](https://marmelab.com/ra-example-oauth) -* [Code Repository](https://github.com/marmelab/ra-example-oauth) - -## Changing The Look And Feel Of React-Admin Without JSX - -This article shows how to customize react-admin look and feel using only the Material UI theme. - -![Screencast](https://marmelab.com/097bee867a1d1dc55dec5456732fe94a/screencast.gif) - -* [Article](https://marmelab.com/blog/2020/09/11/react-admin-tutorials-build-your-own-theme.html) -* [Code Repository](https://github.com/Luwangel/react-admin-tutorials-build-your-own-theme) - -## Build A Custom Tags Selector - -This tutorial explains how to create a custom component to select tags for a record, fetching the list of existing tags and allowing to create new tags on the fly. - -<video controls autoplay playsinline muted loop width="100%"> - <source src="https://marmelab.com/b612ecd6bf066e85bad0a036614f55b0/tags-list-edit.webm" type="video/webm" /> - Your browser does not support the video tag. -</video> - -* [Article](https://marmelab.com/blog/2023/04/26/build-a-custom-tags-selector-with-react-admin.html) -* Test it live in the [CRM Demo](https://marmelab.com/react-admin-crm/#/contacts/1/show) -* CRM Demo [Repository](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/contacts/TagsListEdit.tsx) \ No newline at end of file +If you want to learn the best practices of react-admin development by example, you've come to the right place. This page lists the advanced tutorials we've published on [the react-admin blog](https://marmelab.com/en/blog/#react-admin). + +* 2023-11: [Using React-Admin With DaisyUI, Tailwind CSS, Tanstack Table and React-aria](https://marmelab.com/blog/2023/11/28/using-react-admin-with-your-favorite-ui-library.html) +* 2023-10: [Creating a Record Related to the Current One](https://marmelab.com/blog/2023/10/12/react-admin-v4-advanced-recipes-creating-a-record-related-to-the-current-one.html) +* 2023-09: [Authentication using ActiveDirectory](https://marmelab.com/blog/2023/09/13/active-directory-integration-tutorial.html) +* 2023-08: [Building AI-Augmented Apps](https://marmelab.com/blog/2023/08/09/ai-augmented%20react-apps.html) +* 2023-07: [Building a Kanban Board](https://marmelab.com/blog/2023/07/28/create-a-kanban-board-in-react-admin.html) +* 2023-04: [Building A Custom Tags Selector](https://marmelab.com/blog/2023/04/26/build-a-custom-tags-selector-with-react-admin.html) +* 2023-03: [Creating Custom Form Layouts](https://marmelab.com/blog/2023/03/22/creating-custom-form-layouts-with-react-admin.html) +* 2022-12: [Multi-Tenant Single-Page Apps: Dos and Don'ts](https://marmelab.com/blog/2022/12/14/multitenant-spa.html) +* 2022-11: [Building a B2B app with Strapi and React-Admin](https://marmelab.com/blog/2022/11/28/building-a-crud-app-with-strapi-and-react-admin.html) +* 2022-10: [Writing A Data Provider For Offline-First Applications](https://marmelab.com/blog/2022/10/26/create-an-localforage-dataprovider-in-react-admin.html) +* 2022-04: [Introducing React-admin V4](https://marmelab.com/blog/2022/04/13/react-admin-v4.html) + +Older blog posts contain advice that can be outdated. We keep them here for reference. + +* 2021-12: [Building A Retro React-Admin Theme For Fun And Profit](https://marmelab.com/blog/2021/12/15/retro-admin.html) +* 2021-10: [Using An SQLite Database Live In React-Admin](https://marmelab.com/blog/2021/10/14/using-an-sqlite-database-live-in-react-admin.html) +* 2020-12: [Managing a User Profile](https://marmelab.com/blog/2020/12/14/react-admin-v3-userprofile.html) +* 2020-11: [Changing The Look And Feel Of React-Admin Without JSX](https://marmelab.com/blog/2020/09/11/react-admin-tutorials-build-your-own-theme.html) +* 2020-08: [Handling JWT in Admin Apps the Right Way](https://marmelab.com/blog/2020/07/02/manage-your-jwt-react-admin-authentication-in-memory.html) +* 2020-04: [Custom Forms and UI for related records](https://marmelab.com/blog/2020/04/27/react-admin-tutorials-custom-forms-related-records.html) +* 2019-11: [Introducing React-admin v3](https://marmelab.com/blog/2019/11/20/react-admin-3-0.html) +* 2019-03: [Supplying your own Defaults to React Admin](https://marmelab.com/blog/2019/03/27/supplying-your-own-defaults-to-react-admin.html) +* 2019-03: [Adding a User Profile Page](https://marmelab.com/blog/2019/03/07/react-admin-advanced-recipes-user-profile.html) +* 2019-02: [Creating and Editing a Record From the List Page](https://marmelab.com/blog/2019/02/07/react-admin-advanced-recipes-creating-and-editing-a-record-from-the-list-page.html) +* 2019-01: [Building a Timeline as a replacement for a List component](https://marmelab.com/blog/2019/01/17/react-timeline.html) diff --git a/docs/Features.md b/docs/Features.md index c59a8bd6c87..6446f4cbf77 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -271,7 +271,7 @@ We have made many improvements to this default layout based on user feedback. In And for mobile users, react-admin renders a different layout with larger margins and less information density (see [Responsive](#responsive)). -## Headless +## Headless Core React-admin components use Material UI components by default, which lets you scaffold a page in no time. As material UI supports [theming](#theming), you can easily customize the look and feel of your app. But in some cases, this is not enough, and you need to use another UI library. @@ -358,6 +358,11 @@ Check the following hooks to learn more about headless controllers: - [`useCreateController`](./useCreateController.md) - [`useShowController`](./useShowController.md) +And check these examples for admin panels built with react-admin but without Material UI: + +- [DaisyUI, Tailwind CSS, Tanstack Table and React-Aria](https://marmelab.com/blog/2023/11/28/using-react-admin-with-your-favorite-ui-library.html) +- [shadcn/ui, Tailwind CSS and Radix UI](https://github.com/marmelab/ra-shadcn-demo) + ## Guessers & Scaffolding When mapping a new API route to a CRUD view, adding fields one by one can be tedious. React-admin provides a set of guessers that can automatically **generate a complete CRUD UI based on an API response**. From ab4c65fadec140748b1cf38e85629a59bce0ab40 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Tue, 28 Nov 2023 14:48:37 +0100 Subject: [PATCH 30/72] [Doc] Fix snippets fails to render in JS --- docs/Admin.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/Admin.md b/docs/Admin.md index d77fa00494b..ca4e68d51fe 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -629,7 +629,8 @@ You can also disable the `/login` route completely by passing `false` to this pr const authProvider = { // ... async checkAuth() { - if (/* not authenticated */) { + // ... + if (!authenticated) { throw { redirectTo: '/no-access' }; } }, @@ -689,6 +690,7 @@ If you want to override the react-query default query and mutation default optio ```tsx import { Admin } from 'react-admin'; import { QueryClient } from 'react-query'; +import { dataProvider } from './dataProvider'; const queryClient = new QueryClient({ defaultOptions: { @@ -703,7 +705,7 @@ const queryClient = new QueryClient({ }); const App = () => ( - <Admin queryClient={queryClient} dataProvider={...}> + <Admin queryClient={queryClient} dataProvider={dataProvider}> ... </Admin> ); @@ -896,10 +898,11 @@ But you may want to use another routing strategy, e.g. to allow server-side rend ```tsx import { BrowserRouter } from 'react-router-dom'; import { Admin, Resource } from 'react-admin'; +import { dataProvider } from './dataProvider'; const App = () => ( <BrowserRouter> - <Admin dataProvider={...}> + <Admin dataProvider={dataProvider}> <Resource name="posts" /> </Admin> </BrowserRouter> @@ -915,10 +918,11 @@ However, if you serve your admin from a sub path AND use another Router (like [` ```tsx import { Admin, Resource } from 'react-admin'; import { BrowserRouter } from 'react-router-dom'; +import { dataProvider } from './dataProvider'; const App = () => ( <BrowserRouter> - <Admin basename="/admin" dataProvider={...}> + <Admin basename="/admin" dataProvider={dataProvider}> <Resource name="posts" /> </Admin> </BrowserRouter> @@ -955,9 +959,11 @@ React-admin will have to prefix all the internal links with `/admin`. Use the `< ```tsx // in src/StoreAdmin.js import { Admin, Resource } from 'react-admin'; +import { dataProvider } from './dataProvider'; +import posts from './posts'; export const StoreAdmin = () => ( - <Admin basename="/admin" dataProvider={...}> + <Admin basename="/admin" dataProvider={dataProvider}> <Resource name="posts" {...posts} /> </Admin> ); From 1cabb7addb01b96d3a3315f6d3e0d70dc3409e57 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Tue, 28 Nov 2023 15:03:44 +0100 Subject: [PATCH 31/72] Fix another instance --- docs/DataProviderWriting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DataProviderWriting.md b/docs/DataProviderWriting.md index 79340427bf5..87d60314588 100644 --- a/docs/DataProviderWriting.md +++ b/docs/DataProviderWriting.md @@ -305,7 +305,7 @@ interface DeleteParams { interface DeleteResult { data: Record; } -function delete(resource: string, params: DeleteParams): Promise<DeleteResult> +function _delete(resource: string, params: DeleteParams): Promise<DeleteResult> ``` **Example** From 01ed72ae2101fec9816754ef4b4ffbfae0e55780 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Tue, 28 Nov 2023 17:46:18 +0100 Subject: [PATCH 32/72] [Doc] Fix ts snippet doesn't translate to js --- docs/ListTutorial.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ListTutorial.md b/docs/ListTutorial.md index a59b81c369e..0300cf6e065 100644 --- a/docs/ListTutorial.md +++ b/docs/ListTutorial.md @@ -479,9 +479,10 @@ For these cases, react-admin provides a `<ListGuesser>` component that will gues ```tsx import { Admin, Resource, ListGuesser } from 'react-admin'; +import { dataProvider } from './dataProvider'; const App = () => ( - <Admin dataProvider={...}> + <Admin dataProvider={dataProvider}> <Resource name="posts" list={ListGuesser} /> </Admin> ); From 67ff3ac179cc94e8f99e56a16dede8e126930a46 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 10:28:21 +0100 Subject: [PATCH 33/72] limit react-router version to 6.18.0 --- packages/ra-core/package.json | 4 ++-- packages/react-admin/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index c0684d17924..b968e9fad8e 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -54,8 +54,8 @@ "react": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-hook-form": "^7.43.9", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0" + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0" }, "dependencies": { "clsx": "^1.1.1", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 4111cf5e109..2a05068ba08 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -46,8 +46,8 @@ "ra-language-english": "^4.16.1", "ra-ui-materialui": "^4.16.1", "react-hook-form": "^7.43.9", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0" + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0" }, "gitHead": "b227592132da6ae5f01438fa8269e04596cdfdd8" } From 07673242284ccc37336173b4f4dd4997a7695f3a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 10:37:21 +0100 Subject: [PATCH 34/72] also replace in demos and ra-ui-materialui --- examples/crm/package.json | 4 ++-- examples/demo/package.json | 4 ++-- examples/simple/package.json | 4 ++-- packages/ra-core/package.json | 4 ++-- packages/ra-no-code/package.json | 4 ++-- packages/ra-ui-materialui/package.json | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/crm/package.json b/examples/crm/package.json index 9d42788d91b..a39e3f2ecde 100644 --- a/examples/crm/package.json +++ b/examples/crm/package.json @@ -18,8 +18,8 @@ "react-admin": "^4.12.0", "react-dom": "^17.0.0", "react-error-boundary": "^3.1.4", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0" + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.4", diff --git a/examples/demo/package.json b/examples/demo/package.json index 31b50cc4f88..7d11c48e9f3 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -33,8 +33,8 @@ "react-admin": "^4.12.0", "react-app-polyfill": "^2.0.0", "react-dom": "^17.0.0", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "recharts": "^2.1.15" }, "scripts": { diff --git a/examples/simple/package.json b/examples/simple/package.json index ee86922d017..2d0afe82b72 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -27,8 +27,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-query": "^3.32.1", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0" + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0" }, "devDependencies": { "@babel/preset-react": "^7.12.10", diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index b968e9fad8e..63e59612097 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -40,8 +40,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "react-test-renderer": "^16.9.0 || ^17.0.0", "recharts": "^2.1.15", "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index bed913eaf37..e2aae80a7e3 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -30,8 +30,8 @@ "cross-env": "^5.2.0", "react": "^17.0.0", "react-dom": "^17.0.0", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index b98df63557a..cc0f0892d65 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -42,8 +42,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-is": "^17.0.0", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "react-test-renderer": "~16.8.6", "rimraf": "^3.0.2", "typescript": "^5.1.3" @@ -56,8 +56,8 @@ "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-hook-form": "*", "react-is": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react-router": "^6.1.0", - "react-router-dom": "^6.1.0" + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0" }, "dependencies": { "autosuggest-highlight": "^3.1.1", From a1eb3650ad1e7894f295e3015ccc6a2c3d6d9d58 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 10:43:35 +0100 Subject: [PATCH 35/72] force version to 6.3.0 in devDeps to make sure warnWhenUnsavedChanges works --- packages/ra-core/package.json | 4 +- packages/ra-no-code/package.json | 4 +- packages/ra-ui-materialui/package.json | 4 +- yarn.lock | 85 ++++++++++++++++++-------- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 63e59612097..dc68ca15038 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -40,8 +40,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "~6.3.0", + "react-router-dom": "~6.3.0", "react-test-renderer": "^16.9.0 || ^17.0.0", "recharts": "^2.1.15", "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index e2aae80a7e3..efeb5b84b17 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -30,8 +30,8 @@ "cross-env": "^5.2.0", "react": "^17.0.0", "react-dom": "^17.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "~6.3.0", + "react-router-dom": "~6.3.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index cc0f0892d65..3ecf34524ff 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -42,8 +42,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-is": "^17.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "~6.3.0", + "react-router-dom": "~6.3.0", "react-test-renderer": "~16.8.6", "rimraf": "^3.0.2", "typescript": "^5.1.3" diff --git a/yarn.lock b/yarn.lock index b6c06a7b4d9..f2dd90b5f53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4320,6 +4320,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.11.0": + version: 1.11.0 + resolution: "@remix-run/router@npm:1.11.0" + checksum: 046b382e149e198f4aac02355545ef83c4c97b203d08008dcdb9b25d0f2193e21c3ba2332aba572d4f1450b68aabc77cbc278cb5a7b252cc6e1d239d81a6f7e2 + languageName: node + linkType: hard + "@rushstack/eslint-patch@npm:^1.1.0": version: 1.1.0 resolution: "@rushstack/eslint-patch@npm:1.1.0" @@ -9968,8 +9975,8 @@ __metadata: react-admin: ^4.12.0 react-app-polyfill: ^2.0.0 react-dom: ^17.0.0 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 recharts: ^2.1.15 rewire: ^5.0.0 rollup-plugin-visualizer: ^5.9.2 @@ -18114,8 +18121,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.2 react-query: ^3.32.1 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: ~6.3.0 + react-router-dom: ~6.3.0 react-test-renderer: ^16.9.0 || ^17.0.0 recharts: ^2.1.15 rimraf: ^3.0.2 @@ -18127,8 +18134,8 @@ __metadata: react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 react-hook-form: ^7.43.9 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 languageName: unknown linkType: soft @@ -18348,8 +18355,8 @@ __metadata: react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: ~6.3.0 + react-router-dom: ~6.3.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18391,8 +18398,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.0 react-query: ^3.32.1 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: ~6.3.0 + react-router-dom: ~6.3.0 react-test-renderer: ~16.8.6 react-transition-group: ^4.4.1 rimraf: ^3.0.2 @@ -18405,8 +18412,8 @@ __metadata: react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 react-hook-form: "*" react-is: ^16.9.0 || ^17.0.0 || ^18.0.0 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 languageName: unknown linkType: soft @@ -18501,8 +18508,8 @@ __metadata: react-admin: ^4.12.0 react-dom: ^17.0.0 react-error-boundary: ^3.1.4 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 rollup-plugin-visualizer: ^5.9.2 typescript: ^5.1.3 vite: ^3.2.0 @@ -18577,8 +18584,8 @@ __metadata: ra-language-english: ^4.16.1 ra-ui-materialui: ^4.16.1 react-hook-form: ^7.43.9 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18922,27 +18929,51 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^6.1.0": - version: 6.2.1 - resolution: "react-router-dom@npm:6.2.1" +"react-router-dom@npm:6.1.0 - 6.18.0": + version: 6.18.0 + resolution: "react-router-dom@npm:6.18.0" + dependencies: + "@remix-run/router": 1.11.0 + react-router: 6.18.0 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 9c2893f6b95ca2f25e0ee816d9467fcd18bf1e854d53cde6edf93efbb49972693a898dde6bb8e9ce3c9a2a26f27291b904740e84da9b92a1e560740d5677ce0c + languageName: node + linkType: hard + +"react-router-dom@npm:~6.3.0": + version: 6.3.0 + resolution: "react-router-dom@npm:6.3.0" dependencies: history: ^5.2.0 - react-router: 6.2.1 + react-router: 6.3.0 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: cdec44b06c89f95bb038f964079597ff02a87db62ce231bf61e23500ad75cc321087d06def3b68f38f7805eb84a43fdd0b9f61b45fb3d7f0158d2604da5a538f + checksum: 490b0c50d46c32ad1a3264f8bcaf6b423bef86dc3b62e9070d5e81d90ce086a73af55834133920fc4125e7c8661ede901a550d73429c969b303d4dd9ce7bbaf2 languageName: node linkType: hard -"react-router@npm:6.2.1, react-router@npm:^6.1.0": - version: 6.2.1 - resolution: "react-router@npm:6.2.1" +"react-router@npm:6.1.0 - 6.18.0, react-router@npm:6.18.0": + version: 6.18.0 + resolution: "react-router@npm:6.18.0" + dependencies: + "@remix-run/router": 1.11.0 + peerDependencies: + react: ">=16.8" + checksum: 185d240cb8c6119eefcb16e6df0fb0a75fb736eb1a1f24e7569783286295da48cf66e922c1eee43aa497e464cf2d455b89d1d552e288889ae0c355790b1eac0d + languageName: node + linkType: hard + +"react-router@npm:6.3.0, react-router@npm:~6.3.0": + version: 6.3.0 + resolution: "react-router@npm:6.3.0" dependencies: history: ^5.2.0 peerDependencies: react: ">=16.8" - checksum: 591b4f1fe1c7904b608dfe693b82518c9b453b790421e6ebfec76cee72f7bc2db7a61fb187edf5501484b1f738f090561b8b9ba104b5c1b8757c330c11ece485 + checksum: ac8785a0b28d363940763e49119e5160331099d4f0196235b143ba9cdc984048ca44a77497f393b12165c99baf8ae6c11386f1f6f20ef52d99c2e07b31920862 languageName: node linkType: hard @@ -20146,8 +20177,8 @@ __metadata: react-dom: ^17.0.0 react-hook-form: ^7.43.9 react-query: ^3.32.1 - react-router: ^6.1.0 - react-router-dom: ^6.1.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 typescript: ^5.1.3 vite: ^3.2.0 languageName: unknown From c3f91435d44aa39defc4bd0aae9b441019f993c7 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 11:02:15 +0100 Subject: [PATCH 36/72] use react-router 6.18 for all demos --- packages/ra-core/package.json | 4 +-- packages/ra-no-code/package.json | 4 +-- packages/ra-ui-materialui/package.json | 4 +-- yarn.lock | 38 +++++--------------------- 4 files changed, 13 insertions(+), 37 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index dc68ca15038..63e59612097 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -40,8 +40,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", - "react-router": "~6.3.0", - "react-router-dom": "~6.3.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "react-test-renderer": "^16.9.0 || ^17.0.0", "recharts": "^2.1.15", "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index efeb5b84b17..e2aae80a7e3 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -30,8 +30,8 @@ "cross-env": "^5.2.0", "react": "^17.0.0", "react-dom": "^17.0.0", - "react-router": "~6.3.0", - "react-router-dom": "~6.3.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 3ecf34524ff..cc0f0892d65 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -42,8 +42,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-is": "^17.0.0", - "react-router": "~6.3.0", - "react-router-dom": "~6.3.0", + "react-router": "6.1.0 - 6.18.0", + "react-router-dom": "6.1.0 - 6.18.0", "react-test-renderer": "~16.8.6", "rimraf": "^3.0.2", "typescript": "^5.1.3" diff --git a/yarn.lock b/yarn.lock index f2dd90b5f53..03d8868520b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12785,7 +12785,7 @@ __metadata: languageName: node linkType: hard -"history@npm:^5.1.0, history@npm:^5.2.0": +"history@npm:^5.1.0": version: 5.2.0 resolution: "history@npm:5.2.0" dependencies: @@ -18121,8 +18121,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.2 react-query: ^3.32.1 - react-router: ~6.3.0 - react-router-dom: ~6.3.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 react-test-renderer: ^16.9.0 || ^17.0.0 recharts: ^2.1.15 rimraf: ^3.0.2 @@ -18355,8 +18355,8 @@ __metadata: react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 - react-router: ~6.3.0 - react-router-dom: ~6.3.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18398,8 +18398,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.0 react-query: ^3.32.1 - react-router: ~6.3.0 - react-router-dom: ~6.3.0 + react-router: 6.1.0 - 6.18.0 + react-router-dom: 6.1.0 - 6.18.0 react-test-renderer: ~16.8.6 react-transition-group: ^4.4.1 rimraf: ^3.0.2 @@ -18942,19 +18942,6 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:~6.3.0": - version: 6.3.0 - resolution: "react-router-dom@npm:6.3.0" - dependencies: - history: ^5.2.0 - react-router: 6.3.0 - peerDependencies: - react: ">=16.8" - react-dom: ">=16.8" - checksum: 490b0c50d46c32ad1a3264f8bcaf6b423bef86dc3b62e9070d5e81d90ce086a73af55834133920fc4125e7c8661ede901a550d73429c969b303d4dd9ce7bbaf2 - languageName: node - linkType: hard - "react-router@npm:6.1.0 - 6.18.0, react-router@npm:6.18.0": version: 6.18.0 resolution: "react-router@npm:6.18.0" @@ -18966,17 +18953,6 @@ __metadata: languageName: node linkType: hard -"react-router@npm:6.3.0, react-router@npm:~6.3.0": - version: 6.3.0 - resolution: "react-router@npm:6.3.0" - dependencies: - history: ^5.2.0 - peerDependencies: - react: ">=16.8" - checksum: ac8785a0b28d363940763e49119e5160331099d4f0196235b143ba9cdc984048ca44a77497f393b12165c99baf8ae6c11386f1f6f20ef52d99c2e07b31920862 - languageName: node - linkType: hard - "react-shallow-renderer@npm:^16.13.1": version: 16.14.1 resolution: "react-shallow-renderer@npm:16.14.1" From 529dcb403f6d3199d9f2b1d060122a2a1a0eb778 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 11:31:47 +0100 Subject: [PATCH 37/72] manually edit yarn.lock to go back to version 6.2.1 locally --- yarn.lock | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index 03d8868520b..e3abf4cac61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4320,13 +4320,6 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.11.0": - version: 1.11.0 - resolution: "@remix-run/router@npm:1.11.0" - checksum: 046b382e149e198f4aac02355545ef83c4c97b203d08008dcdb9b25d0f2193e21c3ba2332aba572d4f1450b68aabc77cbc278cb5a7b252cc6e1d239d81a6f7e2 - languageName: node - linkType: hard - "@rushstack/eslint-patch@npm:^1.1.0": version: 1.1.0 resolution: "@rushstack/eslint-patch@npm:1.1.0" @@ -12785,7 +12778,7 @@ __metadata: languageName: node linkType: hard -"history@npm:^5.1.0": +"history@npm:^5.1.0, history@npm:^5.2.0": version: 5.2.0 resolution: "history@npm:5.2.0" dependencies: @@ -18930,26 +18923,26 @@ __metadata: linkType: hard "react-router-dom@npm:6.1.0 - 6.18.0": - version: 6.18.0 - resolution: "react-router-dom@npm:6.18.0" + version: 6.2.1 + resolution: "react-router-dom@npm:6.2.1" dependencies: - "@remix-run/router": 1.11.0 - react-router: 6.18.0 + history: ^5.2.0 + react-router: 6.2.1 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 9c2893f6b95ca2f25e0ee816d9467fcd18bf1e854d53cde6edf93efbb49972693a898dde6bb8e9ce3c9a2a26f27291b904740e84da9b92a1e560740d5677ce0c + checksum: cdec44b06c89f95bb038f964079597ff02a87db62ce231bf61e23500ad75cc321087d06def3b68f38f7805eb84a43fdd0b9f61b45fb3d7f0158d2604da5a538f languageName: node linkType: hard -"react-router@npm:6.1.0 - 6.18.0, react-router@npm:6.18.0": - version: 6.18.0 - resolution: "react-router@npm:6.18.0" +"react-router@npm:6.1.0 - 6.18.0, react-router@npm:6.2.1": + version: 6.2.1 + resolution: "react-router@npm:6.2.1" dependencies: - "@remix-run/router": 1.11.0 + history: ^5.2.0 peerDependencies: react: ">=16.8" - checksum: 185d240cb8c6119eefcb16e6df0fb0a75fb736eb1a1f24e7569783286295da48cf66e922c1eee43aa497e464cf2d455b89d1d552e288889ae0c355790b1eac0d + checksum: 591b4f1fe1c7904b608dfe693b82518c9b453b790421e6ebfec76cee72f7bc2db7a61fb187edf5501484b1f738f090561b8b9ba104b5c1b8757c330c11ece485 languageName: node linkType: hard From ba065051afa7c81a7ca5f6ce936556d191cecfc2 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:39:40 +0100 Subject: [PATCH 38/72] Update CreateInDialogButton documentation --- docs/CreateInDialogButton.md | 224 +++++++++++++++++++++++++++++++++-- docs/EditInDialogButton.md | 4 +- 2 files changed, 217 insertions(+), 11 deletions(-) diff --git a/docs/CreateInDialogButton.md b/docs/CreateInDialogButton.md index b5dd02e3320..57e0edc09d0 100644 --- a/docs/CreateInDialogButton.md +++ b/docs/CreateInDialogButton.md @@ -7,19 +7,29 @@ title: "CreateInDialogButton" This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component offers a way to open a `<Create>` view inside a dialog, hence allowing to create a new record without leaving the current view. -It can be useful in case you want the ability to create a record linked by a reference to the currently edited record, or if you have a nested `<Datagrid>` inside a `<Show>` or an `<Edit>` view. - <video controls autoplay playsinline muted loop> <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-form-layout/latest/CreateInDialogButton.webm" type="video/webm" /> <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-form-layout/latest/CreateInDialogButton.mp4" type="video/mp4" /> Your browser does not support the video tag. </video> +It can be useful in case you want the ability to create a record linked by a reference to the currently edited record, or if you have a nested `<Datagrid>` inside a `<Show>` or an `<Edit>` view. + Note that this component doesn't use routing, so it doesn't change the URL. It's therefore not possible to bookmark the creation dialog, or to link to it from another page. If you need that functionality, use [`<CreateDialog>`](./CreateDialog.md) instead. ## Usage -Put `<CreateInDialogButton>` wherever you would put a `<CreateButton>`, and use the same children as you would for a `<Create>` component (e.g. a `<SimpleForm>`): +First, install the `@react-admin/ra-form-layout` package: + +```sh +npm install --save @react-admin/ra-form-layout +# or +yarn add @react-admin/ra-form-layout +``` + +**Tip**: [`ra-form-layout`](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createindialogbutton-editindialogbutton-and-EditInDialogButton) is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. + +Then, put `<CreateInDialogButton>` wherever you would put a `<CreateButton>`, and use the same children as you would for a `<Create>` component (e.g. a `<SimpleForm>`): {% raw %} ```jsx @@ -63,15 +73,211 @@ const CompanyShow = () => ( In the above example, `<CreateInDialogButton>` is used to create a new employee for the current company. [The `<WithRecord>` component](./WithRecord.md) helps to set the new employee company id by default. +## Props + `<CreateInDialogButton>` accepts the following props: -* `inline`: set to true to display only a Material UI `<IconButton>` instead of the full `<Button>`. The label will still be available as a `<Tooltip>` though. -* `icon`: allows to override the default icon. -* `label`: allows to override the default button label. I18N is supported. -* `ButtonProps`: object containing props to pass to Material UI's `<Button>`. -* remaining props will be passed to the [`<CreateDialog>`](./CreateDialog.md) dialog component. +| Prop | Required | Type | Default | Description | +| -------------- | -------- | ----------------- | ------- | ----------- | +| `children` | Required | `ReactNode` | | The content of the dialog. | +| `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | +| `emptyWhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | +| `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | +| `icon` | Optional | `ReactElement` | | Allows to override the default icon. | +| `inline` | Optional | `boolean` | | Set to true to display only a Material UI `<IconButton>` instead of the full `<Button>`. | +| `label` | Optional | `string` | | Allows to override the default button label. I18N is supported. | +| `maxWidth` | Optional | `string` | `sm` | The max width of the dialog. | +| `mutation Options` | Optional | `object` | | The options to pass to the `useMutation` hook. | +| `resource` | Optional | `string` | | The resource name, e.g. `posts` +| `sx` | Optional | `object` | | Override the styles applied to the dialog component. | + + +## `children` + +`<CreateInDialogButton>` doesn't render any field by default - it delegates this to its children, usually a Form component. + +React-admin provides several built-in form layout components: + +- [`SimpleForm`](./SimpleForm.md) for a single-column layout +- [`TabbedForm`](./TabbedForm.md) for a tabbed layout +- [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections +- [`LongForm`](./LongForm.md) for long forms with a navigation sidebar +- [`WizardForm`](./WizardForm.md) for multi-step forms +- [`CreateDialog`](./CreateDialog.md) for sub-forms in a modal dialog +- and [`Form`](./Form.md), a headless component to use as a base for your custom layouts + +To use an alternative form layout, switch the `<CreateInDialogButton>` child component: + +```diff +const CreateButton = () => ( + <CreateInDialogButton fullWidth maxWidth="md"> +- <SimpleForm> ++ <TabbedForm> ++ <TabbedForm.Tab label="Identity"> + <TextInput source="first_name" fullWidth /> + <TextInput source="last_name" fullWidth /> ++ </TabbedForm.Tab> ++ <TabbedForm.Tab label="Informations"> + <DateInput source="dob" label="born" fullWidth /> + <SelectInput source="sex" choices={sexChoices} fullWidth /> ++ </TabbedForm.Tab> +- </SimpleForm> ++ </TabbedForm> + </CreateInDialogButton> +); +``` + +## `ButtonProps` + +The `ButtonProps` prop allows you to pass props to the MUI `<Button>` component. For instance, to change the color and size of the button: + +{% raw %} +```jsx +const CreateButton = () => ( + <CreateInDialogButton ButtonProps={{ color: 'primary', fullWidth: true }}> + <SimpleForm> + ... + </SimpleForm> + </CreateInDialogButton> +); +``` +{% endraw %} + +## `emptyWhileLoading` + +By default, `<CreateInDialogButton>` renders its child component even before the `dataProvider.getOne()` call returns. If you use `<SimpleForm>` or `<TabbedForm>`, this isn't a problem as these components only render when the record has been fetched. But if you use a custom child component that expects the record context to be defined, your component will throw an error. + +To avoid this, set the `emptyWhileLoading` prop to `true`: + +```jsx +const CreateButton = () => ( + <CreateInDialogButton emptyWhileLoading> + ... + </CreateInDialogButton> +); +``` + +## `fullWidth` + +By default, `<CreateInDialogButton>` renders a [Material UI `<Dialog>`](https://mui.com/material-ui/react-dialog/#full-screen-dialogs) component that takes the width of its content. + +You can make the dialog full width by setting the `fullWidth` prop to `true`: + +```jsx +const CreateButton = () => ( + <CreateInDialogButton fullWidth> + ... + </CreateInDialogButton> +); +``` + +In addition, you can set a dialog maximum width by using the `maxWidth` enumerable in combination with the `fullWidth` boolean. When the `fullWidth` prop is true, the dialog will adapt based on the `maxWidth` value. + +```jsx +const CreateButton = () => ( + <CreateInDialogButton fullWidth maxWidth="sm"> + ... + </CreateInDialogButton> +); +``` + +## `icon` + +The `icon` prop allows you to pass an icon to the button. It can be a MUI icon component, or a custom icon component. + +```jsx +import { Create } from '@mui/icons-material'; + +const CreateButton = () => ( + <CreateInDialogButton icon={<Create />}> + ... + </CreateInDialogButton> +); +``` + +## `inline` + +By default, `<CreateInDialogButton>` renders a `<Button>` component. If you want to display only an `<IconButton>`, set the `inline` prop to `true`: + +```jsx +const CreateButton = () => ( + <CreateInDialogButton inline> + ... + </CreateInDialogButton> +); +``` + +## `label` + +The `label` prop allows you to pass a custom label to the button, instead of the default ("Create"). It can be a string, or a React element. + +```jsx +const CreateButton = () => ( + <CreateInDialogButton label="Edit details"> + ... + </CreateInDialogButton> +); +``` + +## `maxWidth` + +The `maxWidth` prop allows you to set the max width of the dialog. It can be one of the following values: `xs`, `sm`, `md`, `lg`, `xl`, `false`. The default is `sm`. + +For example, you can use that prop to make the dialog full width: + +```jsx +const CreateButton = () => ( + <CreateInDialogButton fullWidth maxWidth={false}> + ... + </CreateInDialogButton> +); +``` + +## `mutationOptions` + +The `mutationOptions` prop allows you to pass options to the `useMutation` hook. -Check out [the `ra-form-layout` documentation](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createindialogbutton-editindialogbutton-and-showindialogbutton) for more details. +This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.create()` call. + +{% raw %} +```jsx +const CreateButton = () => ( + <CreateInDialogButton mutationOptions={{ meta: { fetch: 'author' } }}> + ... + </CreateInDialogButton> +); +``` +{% endraw %} + +## `resource` + +The `resource` prop allows you to pass the resource name to the `<CreateInDialogButton>` component. If not provided, it will be deduced from the resource context. + +This is useful to link to a related record. For instance, the following button lets you create the author of a book: + +```jsx +const CreateAuthorButton = () => { + return ( + <CreateInDialogButton resource="authors"> + ... + </CreateInDialogButton> + ); +}; +``` + +## `sx` + +Customize the styles applied to the Material UI `<Dialog>` component: + +{% raw %} +```jsx +const CreateButton = () => ( + <CreateInDialogButton sx={{ backgroundColor: 'paper' }}> + ... + </CreateInDialogButton> +); +``` +{% endraw %} ## Combining With `<EditInDialogButton>` diff --git a/docs/EditInDialogButton.md b/docs/EditInDialogButton.md index ee88f67ed6e..f39eac61801 100644 --- a/docs/EditInDialogButton.md +++ b/docs/EditInDialogButton.md @@ -74,7 +74,7 @@ const CompanyShow = () => ( | -------------- | -------- | ----------------- | ------- | ----------- | | `children` | Required | `ReactNode` | | The content of the dialog. | | `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | -| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | +| `emptyWhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | | `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | | `icon` | Optional | `ReactElement` | | Allows to override the default icon. | | `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the record context. | @@ -246,7 +246,7 @@ const EditButton = () => ( ## `mutationOptions` -The `queryOptions` prop allows you to pass options to the `useMutation` hook. +The `mutationOptions` prop allows you to pass options to the `useMutation` hook. This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.update()` call. From f865a02c84c947ccfce961fff2f3d6b15caae298 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:01:37 +0100 Subject: [PATCH 39/72] Udate EditDialog documentation --- docs/EditDialog.md | 235 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 217 insertions(+), 18 deletions(-) diff --git a/docs/EditDialog.md b/docs/EditDialog.md index 4d58dee2c13..190f35c5215 100644 --- a/docs/EditDialog.md +++ b/docs/EditDialog.md @@ -15,7 +15,17 @@ This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" s ## Usage -Add the `<EditDialog>` component as a sibling to a `<List>` component. +First, install the `@react-admin/ra-form-layout` package: + +```sh +npm install --save @react-admin/ra-form-layout +# or +yarn add @react-admin/ra-form-layout +``` + +**Tip**: [`ra-form-layout`](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createindialogbutton-editindialogbutton-and-EditInDialogButton) is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. + +Then, add the `<EditDialog>` component as a sibling to a `<List>` component. ```jsx import { @@ -54,23 +64,212 @@ In the related `<Resource>`, you don't need to declare an `edit` component as th <Resource name="customers" list={CustomerList} /> ``` -`<EditDialog>` accepts the same props as the [`<Edit>`](./Edit.md) component, and the same type of children (e.g. a [`<SimpleForm>`](./SimpleForm.md) element). - -* `children`: the components that renders the form -* `className`: passed to the root component -* [`component`](./Edit.md#component): override the root component -* [`disableAuthentication`](./Edit.md#disableauthentication): disable the authentication check -* [`id`](./Edit.md#id): the id of the record to edit -* [`mutationMode`](./Edit.md#mutationmode): switch to optimistic or pessimistic mutations (undoable by default) -* [`mutationOptions`](./Edit.md#mutationoptions): options for the `dataProvider.update()` call -* [`queryOptions`](./Edit.md#queryoptions): options for the `dataProvider.getOne()` call -* [`redirect`](./Edit.md#redirect): change the redirect location after successful creation -* [`resource`](./Edit.md#resource): override the name of the resource to create -* [`sx`](./Edit.md#sx-css-api): Override the styles -* [`title`](./Edit.md#title): override the page title -* [`transform`](./Edit.md#transform): transform the form data before calling `dataProvider.update()` - -Check [the `ra-form-layout` documentation](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createdialog-editdialog--showdialog) for more details. +## Props + +`<EditDialog>` accepts the following props: + +| Prop | Required | Type | Default | Description | +| -------------- | -------- | ----------------- | ------- | ----------- | +| `children` | Required | `ReactNode` | | The content of the dialog. | +| `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | +| `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the record context. | +| `maxWidth` | Optional | `string` | `sm` | The max width of the dialog. | +| `mutation Options` | Optional | `object` | | The options to pass to the `useMutation` hook. | +| `queryOptions` | Optional | `object` | | The options to pass to the `useQuery` hook. +| `resource` | Optional | `string` | | The resource name, e.g. `posts` +| `sx` | Optional | `object` | | Override the styles applied to the dialog component. | +| `transform` | Optional | `function` | | Transform the form data before calling `dataProvider.update()`. | + +## `children` + +`<EditDialog>` doesn't render any field by default - it delegates this to its children, usually a Form component. + +React-admin provides several built-in form layout components: + +- [`SimpleForm`](./SimpleForm.md) for a single-column layout +- [`TabbedForm`](./TabbedForm.md) for a tabbed layout +- [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections +- [`LongForm`](./LongForm.md) for long forms with a navigation sidebar +- [`WizardForm`](./WizardForm.md) for multi-step forms +- [`EditDialog`](./EditDialog.md) for sub-forms in a modal dialog +- and [`Form`](./Form.md), a headless component to use as a base for your custom layouts + +To use an alternative form layout, switch the `<EditInDialogButton>` child component: + +```diff +const MyEditDialog = () => ( + <EditDialog fullWidth maxWidth="md"> +- <SimpleForm> ++ <TabbedForm> ++ <TabbedForm.Tab label="Identity"> + <TextInput source="first_name" fullWidth /> + <TextInput source="last_name" fullWidth /> ++ </TabbedForm.Tab> ++ <TabbedForm.Tab label="Informations"> + <DateInput source="dob" label="born" fullWidth /> + <SelectInput source="sex" choices={sexChoices} fullWidth /> ++ </TabbedForm.Tab> +- </SimpleForm> ++ </TabbedForm> + </EditDialog> +); +``` + +## `fullWidth` + +By default, `<EditDialog>` renders a [Material UI `<Dialog>`](https://mui.com/material-ui/react-dialog/#full-screen-dialogs) component that takes the width of its content. + +You can make the dialog full width by setting the `fullWidth` prop to `true`: + +```jsx +const MyEditDialog = () => ( + <EditDialog fullWidth> + ... + </EditDialog> +); +``` + +In addition, you can set a dialog maximum width by using the `maxWidth` enumerable in combination with the `fullWidth` boolean. When the `fullWidth` prop is true, the dialog will adapt based on the `maxWidth` value. + +```jsx +const MyEditDialog = () => ( + <EditDialog fullWidth maxWidth="sm"> + ... + </EditDialog> +); +``` + +## `id` + +The `id` prop allows you to pass the record id to the `<EditDialog>` component. If not provided, it will be deduced from the record context. + +This is useful to link to a related record. For instance, the following dialog lets you show the author of a book: + +```jsx +const EditAuthorDialog = () => { + const book = useRecordContext(); + return ( + <EditDialog resource="authors" id={book.author_id}> + ... + </EditDialog> + ); +}; +``` + +## `maxWidth` + +The `maxWidth` prop allows you to set the max width of the dialog. It can be one of the following values: `xs`, `sm`, `md`, `lg`, `xl`, `false`. The default is `sm`. + +For example, you can use that prop to make the dialog full width: + +```jsx +const MyEditDialog = () => ( + <EditDialog fullWidth maxWidth={false}> + ... + </EditDialog> +); +``` + +## `mutationOptions` + +The `mutationOptions` prop allows you to pass options to the `useMutation` hook. + +This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.update()` call. + +{% raw %} +```jsx +const MyEditDialog = () => ( + <EditDialog mutationOptions={{ meta: { fetch: 'author' } }}> + ... + </EditDialog> +); +``` +{% endraw %} + +## `queryOptions` + +The `queryOptions` prop allows you to pass options to the `useQuery` hook. + +This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.getOne()` call. + +{% raw %} +```jsx +const MyEditDialog = () => ( + <EditDialog queryOptions={{ meta: { fetch: 'author' } }}> + ... + </EditDialog> +); +``` +{% endraw %} + +## `resource` + +The `resource` prop allows you to pass the resource name to the `<EditDialog>` component. If not provided, it will be deduced from the resource context. + +This is useful to link to a related record. For instance, the following dialog lets you show the author of a book: + +```jsx +const EditAuthorDialog = () => { + const book = useRecordContext(); + return ( + <EditDialog resource="authors" id={book.author_id}> + ... + </EditDialog> + ); +}; +``` + +## `sx` + +Customize the styles applied to the Material UI `<Dialog>` component: + +{% raw %} +```jsx +const EditButton = () => ( + <EditDialog sx={{ backgroundColor: 'paper' }}> + ... + </EditDialog> +); +``` +{% endraw %} + +## `transform` + +To transform a record after the user has submitted the form but before the record is passed to `dataProvider.update()`, use the `transform` prop. It expects a function taking a record as argument, and returning a modified record. For instance, to add a computed field upon edition: + +```jsx +export const UserEdit = () => { + const transform = data => ({ + ...data, + fullName: `${data.firstName} ${data.lastName}` + }); + return ( + <EditDialog transform={transform}> + ... + </EditDialog> + ); +} +``` + +The `transform` function can also return a `Promise`, which allows you to do all sorts of asynchronous calls (e.g. to the `dataProvider`) during the transformation. + +**Tip**: If you want to have different transformations based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save", and another to "save and notify other admins"), you can set the `transform` prop on [the `<SaveButton>` component](./SaveButton.md), too. + +**Tip**: The `transform` function also get the `previousData` in its second argument: + +```jsx +export const UserEdit = () => { + const transform = (data, { previousData }) => ({ + ...data, + avoidChangeField: previousData.avoidChangeField + }); + return ( + <EditDialog transform={transform}> + ... + </EditDialog> + ); +} +``` ## Usage Without Routing From c4d2b54a28fadcc047550f377cd4d7b1e19fd79f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:06:10 +0100 Subject: [PATCH 40/72] Udate CreateDialog documentation --- docs/CreateDialog.md | 200 +++++++++++++++++++++++++++++++++++++++---- docs/EditDialog.md | 6 +- 2 files changed, 187 insertions(+), 19 deletions(-) diff --git a/docs/CreateDialog.md b/docs/CreateDialog.md index eb92cc880e6..3949ecf99d8 100644 --- a/docs/CreateDialog.md +++ b/docs/CreateDialog.md @@ -15,7 +15,18 @@ This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" s ## Usage -Add the `<CreateDialog>` component as a sibling to a `<List>` component. + +First, install the `@react-admin/ra-form-layout` package: + +```sh +npm install --save @react-admin/ra-form-layout +# or +yarn add @react-admin/ra-form-layout +``` + +**Tip**: [`ra-form-layout`](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createdialog-editdialog--showdialog) is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. + +Then, add the `<CreateDialog>` component as a sibling to a `<List>` component. ```jsx import { @@ -57,21 +68,178 @@ In the related `<Resource>`, you don't need to declare a `create` component as t **Note**: You can't use the `<CreateDialog>` and have a standard `<Edit>` specified on your `<Resource>`, because the `<Routes>` declarations would conflict. If you need this, use the [`<CreateInDialogButton>`](./CreateInDialogButton.md) instead. -`<CreateDialog>` accepts the same props as the [`<Create>`](./Create.md) component, and the same type of children (e.g. a [`<SimpleForm>`](./SimpleForm.md) element). - -* `children`: the components that renders the form -* `className`: passed to the root component -* [`component`](./Create.md#component): override the root component -* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check -* [`mutationOptions`](./Create.md#mutationoptions): options for the `dataProvider.create()` call -* [`record`](./Create.md#record): initialize the form with a record -* [`redirect`](./Create.md#redirect): change the redirect location after successful creation -* [`resource`](./Create.md#resource): override the name of the resource to create -* [`sx`](./Create.md#sx-css-api): Override the styles -* [`title`](./Create.md#title): override the page title -* [`transform`](./Create.md#transform): transform the form data before calling `dataProvider.create()` - -Check [the `ra-form-layout` documentation](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createdialog-editdialog--showdialog) for more details. + +## Props + +`<CreateDialog>` accepts the following props: + +| Prop | Required | Type | Default | Description | +| -------------- | -------- | ----------------- | ------- | ----------- | +| `children` | Required | `ReactNode` | | The content of the dialog. | +| `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | +| `maxWidth` | Optional | `string` | `sm` | The max width of the dialog. | +| `mutation Options` | Optional | `object` | | The options to pass to the `useMutation` hook. | +| `resource` | Optional | `string` | | The resource name, e.g. `posts` +| `sx` | Optional | `object` | | Override the styles applied to the dialog component. | +| `transform` | Optional | `function` | | Transform the form data before calling `dataProvider.update()`. | + +## `children` + +`<CreateDialog>` doesn't render any field by default - it delegates this to its children, usually a Form component. + +React-admin provides several built-in form layout components: + +- [`SimpleForm`](./SimpleForm.md) for a single-column layout +- [`TabbedForm`](./TabbedForm.md) for a tabbed layout +- [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections +- [`LongForm`](./LongForm.md) for long forms with a navigation sidebar +- [`WizardForm`](./WizardForm.md) for multi-step forms +- [`CreateDialog`](./CreateDialog.md) for sub-forms in a modal dialog +- and [`Form`](./Form.md), a headless component to use as a base for your custom layouts + +To use an alternative form layout, switch the `<CreateDialog>` child component: + +```diff +const MyCreateDialog = () => ( + <CreateDialog fullWidth maxWidth="md"> +- <SimpleForm> ++ <TabbedForm> ++ <TabbedForm.Tab label="Identity"> + <TextInput source="first_name" fullWidth /> + <TextInput source="last_name" fullWidth /> ++ </TabbedForm.Tab> ++ <TabbedForm.Tab label="Informations"> + <DateInput source="dob" label="born" fullWidth /> + <SelectInput source="sex" choices={sexChoices} fullWidth /> ++ </TabbedForm.Tab> +- </SimpleForm> ++ </TabbedForm> + </CreateDialog> +); +``` + +## `fullWidth` + +By default, `<CreateDialog>` renders a [Material UI `<Dialog>`](https://mui.com/material-ui/react-dialog/#full-screen-dialogs) component that takes the width of its content. + +You can make the dialog full width by setting the `fullWidth` prop to `true`: + +```jsx +const MyCreateDialog = () => ( + <CreateDialog fullWidth> + ... + </CreateDialog> +); +``` + +In addition, you can set a dialog maximum width by using the `maxWidth` enumerable in combination with the `fullWidth` boolean. When the `fullWidth` prop is true, the dialog will adapt based on the `maxWidth` value. + +```jsx +const MyCreateDialog = () => ( + <CreateDialog fullWidth maxWidth="sm"> + ... + </CreateDialog> +); +``` + +## `maxWidth` + +The `maxWidth` prop allows you to set the max width of the dialog. It can be one of the following values: `xs`, `sm`, `md`, `lg`, `xl`, `false`. The default is `sm`. + +For example, you can use that prop to make the dialog full width: + +```jsx +const MyCreateDialog = () => ( + <CreateDialog fullWidth maxWidth={false}> + ... + </CreateDialog> +); +``` + +## `mutationOptions` + +The `mutationOptions` prop allows you to pass options to the `useMutation` hook. + +This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.update()` call. + +{% raw %} +```jsx +const MyCreateDialog = () => ( + <CreateDialog mutationOptions={{ meta: { fetch: 'author' } }}> + ... + </CreateDialog> +); +``` +{% endraw %} + +## `resource` + +The `resource` prop allows you to pass the resource name to the `<CreateDialog>` component. If not provided, it will be deduced from the resource context. + +This is useful to link to a related record. For instance, the following dialog lets you create the author of a book: + +```jsx +const EditAuthorDialog = () => { + const book = useRecordContext(); + return ( + <CreateDialog resource="authors"> + ... + </CreateDialog> + ); +}; +``` + +## `sx` + +Customize the styles applied to the Material UI `<Dialog>` component: + +{% raw %} +```jsx +const MyCreateDialog = () => ( + <CreateDialog sx={{ backgroundColor: 'paper' }}> + ... + </CreateDialog> +); +``` +{% endraw %} + +## `transform` + +To transform a record after the user has submitted the form but before the record is passed to `dataProvider.create()`, use the `transform` prop. It expects a function taking a record as argument, and returning a modified record. For instance, to add a computed field upon edition: + +```jsx +export const UseCreate = () => { + const transform = data => ({ + ...data, + fullName: `${data.firstName} ${data.lastName}` + }); + return ( + <CreateDialog transform={transform}> + ... + </CreateDialog> + ); +} +``` + +The `transform` function can also return a `Promise`, which allows you to do all sorts of asynchronous calls (e.g. to the `dataProvider`) during the transformation. + +**Tip**: If you want to have different transformations based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save", and another to "save and notify other admins"), you can set the `transform` prop on [the `<SaveButton>` component](./SaveButton.md), too. + +**Tip**: The `transform` function also get the `previousData` in its second argument: + +```jsx +export const UseCreate = () => { + const transform = (data, { previousData }) => ({ + ...data, + avoidChangeField: previousData.avoidChangeField + }); + return ( + <CreateDialog transform={transform}> + ... + </CreateDialog> + ); +} +``` ## Usage Without Routing diff --git a/docs/EditDialog.md b/docs/EditDialog.md index 190f35c5215..9dc35b664e8 100644 --- a/docs/EditDialog.md +++ b/docs/EditDialog.md @@ -23,7 +23,7 @@ npm install --save @react-admin/ra-form-layout yarn add @react-admin/ra-form-layout ``` -**Tip**: [`ra-form-layout`](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createindialogbutton-editindialogbutton-and-EditInDialogButton) is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. +**Tip**: [`ra-form-layout`](https://marmelab.com/ra-enterprise/modules/ra-form-layout#createdialog-editdialog--showdialog) is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. Then, add the `<EditDialog>` component as a sibling to a `<List>` component. @@ -94,7 +94,7 @@ React-admin provides several built-in form layout components: - [`EditDialog`](./EditDialog.md) for sub-forms in a modal dialog - and [`Form`](./Form.md), a headless component to use as a base for your custom layouts -To use an alternative form layout, switch the `<EditInDialogButton>` child component: +To use an alternative form layout, switch the `<EditDialog>` child component: ```diff const MyEditDialog = () => ( @@ -225,7 +225,7 @@ Customize the styles applied to the Material UI `<Dialog>` component: {% raw %} ```jsx -const EditButton = () => ( +const MyEditDialog = () => ( <EditDialog sx={{ backgroundColor: 'paper' }}> ... </EditDialog> From eb7471ba7b8c84310608ae367ba0b2955d5dd8e7 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:07:13 +0100 Subject: [PATCH 41/72] Fix formatting --- docs/CreateInDialogButton.md | 2 +- docs/EditInDialogButton.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CreateInDialogButton.md b/docs/CreateInDialogButton.md index 57e0edc09d0..5c4369ba0d5 100644 --- a/docs/CreateInDialogButton.md +++ b/docs/CreateInDialogButton.md @@ -81,7 +81,7 @@ In the above example, `<CreateInDialogButton>` is used to create a new employee | -------------- | -------- | ----------------- | ------- | ----------- | | `children` | Required | `ReactNode` | | The content of the dialog. | | `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | -| `emptyWhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | +| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | | `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | | `icon` | Optional | `ReactElement` | | Allows to override the default icon. | | `inline` | Optional | `boolean` | | Set to true to display only a Material UI `<IconButton>` instead of the full `<Button>`. | diff --git a/docs/EditInDialogButton.md b/docs/EditInDialogButton.md index f39eac61801..887358032c3 100644 --- a/docs/EditInDialogButton.md +++ b/docs/EditInDialogButton.md @@ -74,7 +74,7 @@ const CompanyShow = () => ( | -------------- | -------- | ----------------- | ------- | ----------- | | `children` | Required | `ReactNode` | | The content of the dialog. | | `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | -| `emptyWhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | +| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | | `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | | `icon` | Optional | `ReactElement` | | Allows to override the default icon. | | `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the record context. | From 5ec7fa3b26c596181675161244e8a8d2462947d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Fri, 1 Dec 2023 00:24:10 +0100 Subject: [PATCH 42/72] Fix `useUpdateMany` doesn't accept the `returnPromise` option at call time But `useUpdate` does support it. --- packages/ra-core/src/dataProvider/useUpdateMany.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.ts b/packages/ra-core/src/dataProvider/useUpdateMany.ts index ac7762b8b77..409263d5bf8 100644 --- a/packages/ra-core/src/dataProvider/useUpdateMany.ts +++ b/packages/ra-core/src/dataProvider/useUpdateMany.ts @@ -305,7 +305,7 @@ export const useUpdateMany = < ) => { const { mutationMode, - returnPromise, + returnPromise = reactMutationOptions.returnPromise, onSuccess, onSettled, onError, @@ -458,7 +458,7 @@ export type UseUpdateManyOptions< Array<RecordType['id']>, MutationError, Partial<UseUpdateManyMutateParams<RecordType>> -> & { mutationMode?: MutationMode }; +> & { mutationMode?: MutationMode; returnPromise?: boolean }; export type UseUpdateManyResult< RecordType extends RaRecord = any, From d46b247252f38c97f6ebf359162789ac6febe387 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Fri, 1 Dec 2023 10:46:57 +0100 Subject: [PATCH 43/72] [Doc] Update Search doc to include TextField props --- docs/Search.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Search.md b/docs/Search.md index 9efd5bd352c..a3653655e9d 100644 --- a/docs/Search.md +++ b/docs/Search.md @@ -169,6 +169,8 @@ The `<Search>` component accepts the following props: | `options` | Optional | `Object` | - | An object containing options to apply to the search. | | `wait` | Optional | `number` | 500 | The delay of debounce for the search to launch after typing in ms. | +Additional props are passed down to the Material UI [`<TextField>`](https://mui.com/material-ui/react-text-field/) component. + ## `children` The `<Search>` children allow you to customize the way results are displayed. The child component can grab the search result using the `useSearchResult` hook. From a6f359811b1a72f28fc62e5357932627036e1ea6 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Fri, 1 Dec 2023 11:57:14 +0100 Subject: [PATCH 44/72] Fix `ra-data-graphql` uses a Proxy, which prevents adding more methods automatically --- packages/ra-data-graphql/README.md | 128 +++++++++--------- packages/ra-data-graphql/src/index.ts | 181 +++++++++++++------------- 2 files changed, 149 insertions(+), 160 deletions(-) diff --git a/packages/ra-data-graphql/README.md b/packages/ra-data-graphql/README.md index 3658d014875..1d9ed087345 100644 --- a/packages/ra-data-graphql/README.md +++ b/packages/ra-data-graphql/README.md @@ -39,39 +39,35 @@ yarn add graphql ra-data-graphql ## Usage +Build the data provider on mount, and pass it to the `<Admin>` component when ready: + ```jsx // in App.js import * as React from 'react'; -import { Component } from 'react'; +import { useState, useEffect } from 'react'; import buildGraphQLProvider from 'ra-data-graphql'; -import { Admin, Resource, Delete } from 'react-admin'; +import { Admin, Resource } from 'react-admin'; import buildQuery from './buildQuery'; // see Specify your queries and mutations section below import { PostCreate, PostEdit, PostList } from '../components/admin/posts'; -class App extends Component { - constructor() { - super(); - this.state = { dataProvider: null }; - } - componentDidMount() { - buildGraphQLProvider({ buildQuery }) - .then(dataProvider => this.setState({ dataProvider })); - } - - render() { - const { dataProvider } = this.state; +const App = () => { + const [dataProvider, setDataProvider] = useState(null); - if (!dataProvider) { - return <div>Loading</div>; - } + useEffect(() => { + buildGraphQLProvider({ buildQuery }) + .then(dataProvider => setDataProvider(dataProvider)); + }, []); - return ( - <Admin dataProvider={dataProvider}> - <Resource name="Post" list={PostList} edit={PostEdit} create={PostCreate} /> - </Admin> - ); + if (!dataProvider) { + return <div>Loading</div>; } + + return ( + <Admin dataProvider={dataProvider}> + <Resource name="Post" list={PostList} edit={PostEdit} create={PostCreate} /> + </Admin> + ); } export default App; @@ -79,51 +75,11 @@ export default App; ## Options -### Customize the Apollo client - -You can specify the client options by calling `buildGraphQLProvider` like this: - -```js -import { createNetworkInterface } from 'react-apollo'; - -buildGraphQLProvider({ - client: { - networkInterface: createNetworkInterface({ - uri: 'http://api.myproduct.com/graphql', - }), - }, -}); -``` - -You can pass any options supported by the [ApolloClient](https://www.apollographql.com/docs/react/api/core/ApolloClient/) constructor with the addition of `uri` which can be specified so that we create the network interface for you. Pass those options as `clientOptions`. - -You can also supply your own [ApolloClient](https://www.apollographql.com/docs/react/api/core/ApolloClient/) instance directly with: - -```js -buildGraphQLProvider({ client: myClient }); -``` - -### Introspection Options - -Instead of running an introspection query you can also provide the introspection query result directly. This speeds up the initial rendering of the `Admin` component as it no longer has to wait for the introspection query request to resolve. - -```js -import { __schema as schema } from './schema'; - -buildGraphQLProvider({ - introspection: { schema } -}); -``` - -The `./schema` file is a `schema.json` in `./src` retrieved with [get-graphql-schema --json <graphql_endpoint>](https://github.com/graphcool/get-graphql-schema). - -> Note: Importing the `schema.json` file will significantly increase the bundle size. +## Specify queries and mutations -## Specify your queries and mutations +For the provider to know how to map react-admin request to apollo queries and mutations, you must provide a `buildQuery` option. The `buildQuery` is a factory function that will be called with the introspection query result. -For the provider to know how to map react-admin request to apollo queries and mutations, you must provide a `buildQuery` option. The `buildQuery` is a factory function which will be called with the introspection query result. - -The introspection result is an object with 4 properties: +As a reminder, the result of a GraphQL introspection query is an object with 4 properties: - `types`: an array of all the GraphQL types discovered on your endpoint - `queries`: an array of all the GraphQL queries and mutations discovered on your endpoint @@ -181,7 +137,7 @@ For example: } ``` -The `buildQuery` function must return a function which will be called with the same parameters as the react-admin data provider, but must return an object matching the `options` of the ApolloClient [query](http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.query) method with an additional `parseResponse` function. +The `buildQuery` function receives this object and must return a function which will be called with the same parameters as the react-admin data provider, but must return an object matching the `options` of the ApolloClient [query](http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.query) method with an additional `parseResponse` function. This `parseResponse` function will be called with an [ApolloQueryResult](http://dev.apollodata.com/core/apollo-client-api.html#ApolloQueryResult) and must return the data expected by react-admin. @@ -214,6 +170,46 @@ const buildQuery = introspectionResults => (raFetchType, resourceName, params) = buildGraphQLProvider({ buildQuery }); ``` +### Customize the Apollo client + +You can specify the client options by calling `buildGraphQLProvider` like this: + +```js +import { createNetworkInterface } from 'react-apollo'; + +buildGraphQLProvider({ + client: { + networkInterface: createNetworkInterface({ + uri: 'http://api.myproduct.com/graphql', + }), + }, +}); +``` + +You can pass any options supported by the [ApolloClient](https://www.apollographql.com/docs/react/api/core/ApolloClient/) constructor with the addition of `uri` which can be specified so that we create the network interface for you. Pass those options as `clientOptions`. + +You can also supply your own [ApolloClient](https://www.apollographql.com/docs/react/api/core/ApolloClient/) instance directly with: + +```js +buildGraphQLProvider({ client: myClient }); +``` + +### Introspection Options + +Instead of running an introspection query you can also provide the introspection query result directly. This speeds up the initial rendering of the `Admin` component as it no longer has to wait for the introspection query request to resolve. + +```js +import { __schema as schema } from './schema'; + +buildGraphQLProvider({ + introspection: { schema } +}); +``` + +The `./schema` file is a `schema.json` in `./src` retrieved with [get-graphql-schema --json <graphql_endpoint>](https://github.com/graphcool/get-graphql-schema). + +> Note: Importing the `schema.json` file will significantly increase the bundle size. + ## Troubleshooting ## When I create or edit a resource, the list or edit page does not refresh its data diff --git a/packages/ra-data-graphql/src/index.ts b/packages/ra-data-graphql/src/index.ts index 8f86eb748a1..28dc0f65430 100644 --- a/packages/ra-data-graphql/src/index.ts +++ b/packages/ra-data-graphql/src/index.ts @@ -43,17 +43,19 @@ export const QUERY_TYPES = INNER_QUERY_TYPES; export const MUTATION_TYPES = INNER_MUTATION_TYPES; export const ALL_TYPES = INNER_ALL_TYPES; -const RaFetchMethodMap = { - getList: GET_LIST, - getMany: GET_MANY, - getManyReference: GET_MANY_REFERENCE, - getOne: GET_ONE, - create: CREATE, - delete: DELETE, - deleteMany: DELETE_MANY, - update: UPDATE, - updateMany: UPDATE_MANY, -}; +/** + * Map dataProvider method names to GraphQL queries and mutations + * + * @example for the Customer resource: + * dataProvider.getList() // query allCustomers() { ... } + * dataProvider.getOne() // query Customer($id: id) { ... } + * dataProvider.getMany() // query allCustomers($filter: { ids: [ids] }) { ... } + * dataProvider.getManyReference() // query allCustomers($filter: { [target]: [id] }) { ... } + * dataProvider.create() // mutation createCustomer($firstName: firstName, $lastName: lastName, ...) { ... } + * dataProvider.update() // mutation updateCustomer($id: id, firstName: firstName, $lastName: lastName, ...) { ... } + * dataProvider.delete() // mutation deleteCustomer($id: id) { ... } + * // note that updateMany and deleteMany aren't mapped in this adapter + */ const defaultOptions = { resolveIntrospection: introspectSchema, introspection: { @@ -125,7 +127,10 @@ export type Options = { watchQuery?: GetWatchQueryOptions; }; -export default async (options: Options): Promise<DataProvider> => { +// @FIXME in v5: This doesn't need to be an async method +const buildGraphQLProvider = async ( + options: Options +): Promise<DataProvider> => { const { client: clientObject, clientOptions, @@ -148,79 +153,78 @@ export default async (options: Options): Promise<DataProvider> => { let introspectionResults; let introspectionResultsPromise; - const raDataProvider = new Proxy<DataProvider>(defaultDataProvider, { - get: (target, name) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - const raFetchMethod = RaFetchMethodMap[name]; - return async (resource, params) => { - if (introspection) { - if (!introspectionResultsPromise) { - introspectionResultsPromise = resolveIntrospection( - client, - introspection - ); - } - - introspectionResults = await introspectionResultsPromise; - } - - const buildQuery = buildQueryFactory(introspectionResults); - const overriddenBuildQuery = get( - override, - `${resource}.${raFetchMethod}` + const callApollo = async (raFetchMethod, resource, params) => { + if (introspection) { + if (!introspectionResultsPromise) { + introspectionResultsPromise = resolveIntrospection( + client, + introspection ); + } - const { parseResponse, ...query } = overriddenBuildQuery - ? { - ...buildQuery(raFetchMethod, resource, params), - ...overriddenBuildQuery(params), - } - : buildQuery(raFetchMethod, resource, params); - - const operation = getQueryOperation(query.query); - - if (operation === 'query') { - const apolloQuery = { - ...query, - fetchPolicy: 'network-only', - ...getOptions( - otherOptions.query, - raFetchMethod, - resource - ), - }; - - return ( - client - // @ts-ignore - .query(apolloQuery) - .then(response => parseResponse(response)) - .catch(handleError) - ); - } - - const apolloQuery = { - mutation: query.query, - variables: query.variables, - ...getOptions( - otherOptions.mutation, - raFetchMethod, - resource - ), - }; - - return ( - client - // @ts-ignore - .mutate(apolloQuery) - .then(parseResponse) - .catch(handleError) - ); + introspectionResults = await introspectionResultsPromise; + } + + const buildQuery = buildQueryFactory(introspectionResults); + const overriddenBuildQuery = get( + override, + `${resource}.${raFetchMethod}` + ); + + const { parseResponse, ...query } = overriddenBuildQuery + ? { + ...buildQuery(raFetchMethod, resource, params), + ...overriddenBuildQuery(params), + } + : buildQuery(raFetchMethod, resource, params); + + const operation = getQueryOperation(query.query); + + if (operation === 'query') { + const apolloQuery = { + ...query, + fetchPolicy: 'network-only', + ...getOptions(otherOptions.query, raFetchMethod, resource), }; - }, - }); + + return ( + client + // @ts-ignore + .query(apolloQuery) + .then(response => parseResponse(response)) + .catch(handleError) + ); + } + + const apolloQuery = { + mutation: query.query, + variables: query.variables, + ...getOptions(otherOptions.mutation, raFetchMethod, resource), + }; + + return ( + client + // @ts-ignore + .mutate(apolloQuery) + .then(parseResponse) + .catch(handleError) + ); + }; + + const raDataProvider = { + create: (resource, params) => callApollo(CREATE, resource, params), + delete: (resource, params) => callApollo(DELETE, resource, params), + deleteMany: (resource, params) => + callApollo(DELETE_MANY, resource, params), + getList: (resource, params) => callApollo(GET_LIST, resource, params), + getMany: (resource, params) => callApollo(GET_MANY, resource, params), + getManyReference: (resource, params) => + callApollo(GET_MANY_REFERENCE, resource, params), + getOne: (resource, params) => callApollo(GET_ONE, resource, params), + update: (resource, params) => callApollo(UPDATE, resource, params), + updateMany: (resource, params) => + callApollo(UPDATE_MANY, resource, params), + }; return raDataProvider; }; @@ -244,15 +248,4 @@ const getQueryOperation = query => { throw new Error('Unable to determine the query operation'); }; -// Only used to initialize proxy -const defaultDataProvider = { - create: () => Promise.resolve({ data: null }), // avoids adding a context in tests - delete: () => Promise.resolve({ data: null }), // avoids adding a context in tests - deleteMany: () => Promise.resolve({ data: [] }), // avoids adding a context in tests - getList: () => Promise.resolve({ data: [], total: 0 }), // avoids adding a context in tests - getMany: () => Promise.resolve({ data: [] }), // avoids adding a context in tests - getManyReference: () => Promise.resolve({ data: [], total: 0 }), // avoids adding a context in tests - getOne: () => Promise.resolve({ data: null }), // avoids adding a context in tests - update: () => Promise.resolve({ data: null }), // avoids adding a context in tests - updateMany: () => Promise.resolve({ data: [] }), // avoids adding a context in tests -}; +export default buildGraphQLProvider; From ad466bd710080833951a45cb3751becfda6943a0 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto <fzaninotto@gmail.com> Date: Fri, 1 Dec 2023 23:00:14 +0100 Subject: [PATCH 45/72] Revert "Fix TabbedForm and TabbedShowLayout tab navigation: limit react-router version to 6.18.0 as a workaround" --- examples/crm/package.json | 4 +-- examples/demo/package.json | 4 +-- examples/simple/package.json | 4 +-- packages/ra-core/package.json | 8 +++--- packages/ra-no-code/package.json | 4 +-- packages/ra-ui-materialui/package.json | 8 +++--- packages/react-admin/package.json | 4 +-- yarn.lock | 40 +++++++++++++------------- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/examples/crm/package.json b/examples/crm/package.json index a39e3f2ecde..9d42788d91b 100644 --- a/examples/crm/package.json +++ b/examples/crm/package.json @@ -18,8 +18,8 @@ "react-admin": "^4.12.0", "react-dom": "^17.0.0", "react-error-boundary": "^3.1.4", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0" + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.4", diff --git a/examples/demo/package.json b/examples/demo/package.json index 7d11c48e9f3..31b50cc4f88 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -33,8 +33,8 @@ "react-admin": "^4.12.0", "react-app-polyfill": "^2.0.0", "react-dom": "^17.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0", "recharts": "^2.1.15" }, "scripts": { diff --git a/examples/simple/package.json b/examples/simple/package.json index 2d0afe82b72..ee86922d017 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -27,8 +27,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-query": "^3.32.1", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0" + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0" }, "devDependencies": { "@babel/preset-react": "^7.12.10", diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 63e59612097..c0684d17924 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -40,8 +40,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0", "react-test-renderer": "^16.9.0 || ^17.0.0", "recharts": "^2.1.15", "rimraf": "^3.0.2", @@ -54,8 +54,8 @@ "react": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-hook-form": "^7.43.9", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0" + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0" }, "dependencies": { "clsx": "^1.1.1", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index e2aae80a7e3..bed913eaf37 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -30,8 +30,8 @@ "cross-env": "^5.2.0", "react": "^17.0.0", "react-dom": "^17.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index cc0f0892d65..b98df63557a 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -42,8 +42,8 @@ "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-is": "^17.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0", + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0", "react-test-renderer": "~16.8.6", "rimraf": "^3.0.2", "typescript": "^5.1.3" @@ -56,8 +56,8 @@ "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-hook-form": "*", "react-is": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0" + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0" }, "dependencies": { "autosuggest-highlight": "^3.1.1", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 2a05068ba08..4111cf5e109 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -46,8 +46,8 @@ "ra-language-english": "^4.16.1", "ra-ui-materialui": "^4.16.1", "react-hook-form": "^7.43.9", - "react-router": "6.1.0 - 6.18.0", - "react-router-dom": "6.1.0 - 6.18.0" + "react-router": "^6.1.0", + "react-router-dom": "^6.1.0" }, "gitHead": "b227592132da6ae5f01438fa8269e04596cdfdd8" } diff --git a/yarn.lock b/yarn.lock index e3abf4cac61..b6c06a7b4d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9968,8 +9968,8 @@ __metadata: react-admin: ^4.12.0 react-app-polyfill: ^2.0.0 react-dom: ^17.0.0 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 recharts: ^2.1.15 rewire: ^5.0.0 rollup-plugin-visualizer: ^5.9.2 @@ -18114,8 +18114,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.2 react-query: ^3.32.1 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 react-test-renderer: ^16.9.0 || ^17.0.0 recharts: ^2.1.15 rimraf: ^3.0.2 @@ -18127,8 +18127,8 @@ __metadata: react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 react-hook-form: ^7.43.9 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 languageName: unknown linkType: soft @@ -18348,8 +18348,8 @@ __metadata: react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18391,8 +18391,8 @@ __metadata: react-hook-form: ^7.43.9 react-is: ^17.0.0 react-query: ^3.32.1 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 react-test-renderer: ~16.8.6 react-transition-group: ^4.4.1 rimraf: ^3.0.2 @@ -18405,8 +18405,8 @@ __metadata: react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 react-hook-form: "*" react-is: ^16.9.0 || ^17.0.0 || ^18.0.0 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 languageName: unknown linkType: soft @@ -18501,8 +18501,8 @@ __metadata: react-admin: ^4.12.0 react-dom: ^17.0.0 react-error-boundary: ^3.1.4 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 rollup-plugin-visualizer: ^5.9.2 typescript: ^5.1.3 vite: ^3.2.0 @@ -18577,8 +18577,8 @@ __metadata: ra-language-english: ^4.16.1 ra-ui-materialui: ^4.16.1 react-hook-form: ^7.43.9 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18922,7 +18922,7 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.1.0 - 6.18.0": +"react-router-dom@npm:^6.1.0": version: 6.2.1 resolution: "react-router-dom@npm:6.2.1" dependencies: @@ -18935,7 +18935,7 @@ __metadata: languageName: node linkType: hard -"react-router@npm:6.1.0 - 6.18.0, react-router@npm:6.2.1": +"react-router@npm:6.2.1, react-router@npm:^6.1.0": version: 6.2.1 resolution: "react-router@npm:6.2.1" dependencies: @@ -20146,8 +20146,8 @@ __metadata: react-dom: ^17.0.0 react-hook-form: ^7.43.9 react-query: ^3.32.1 - react-router: 6.1.0 - 6.18.0 - react-router-dom: 6.1.0 - 6.18.0 + react-router: ^6.1.0 + react-router-dom: ^6.1.0 typescript: ^5.1.3 vite: ^3.2.0 languageName: unknown From 1528349252ff81322d581b2636e1f1369843f80d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:41:43 +0100 Subject: [PATCH 46/72] Apply review suggestions --- docs/CreateDialog.md | 7 +++---- docs/CreateInDialogButton.md | 16 ---------------- docs/EditDialog.md | 2 +- docs/EditInDialogButton.md | 16 ---------------- 4 files changed, 4 insertions(+), 37 deletions(-) diff --git a/docs/CreateDialog.md b/docs/CreateDialog.md index 3949ecf99d8..1355c8b9eaa 100644 --- a/docs/CreateDialog.md +++ b/docs/CreateDialog.md @@ -81,7 +81,7 @@ In the related `<Resource>`, you don't need to declare a `create` component as t | `mutation Options` | Optional | `object` | | The options to pass to the `useMutation` hook. | | `resource` | Optional | `string` | | The resource name, e.g. `posts` | `sx` | Optional | `object` | | Override the styles applied to the dialog component. | -| `transform` | Optional | `function` | | Transform the form data before calling `dataProvider.update()`. | +| `transform` | Optional | `function` | | Transform the form data before calling `dataProvider.create()`. | ## `children` @@ -94,7 +94,6 @@ React-admin provides several built-in form layout components: - [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections - [`LongForm`](./LongForm.md) for long forms with a navigation sidebar - [`WizardForm`](./WizardForm.md) for multi-step forms -- [`CreateDialog`](./CreateDialog.md) for sub-forms in a modal dialog - and [`Form`](./Form.md), a headless component to use as a base for your custom layouts To use an alternative form layout, switch the `<CreateDialog>` child component: @@ -160,7 +159,7 @@ const MyCreateDialog = () => ( The `mutationOptions` prop allows you to pass options to the `useMutation` hook. -This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.update()` call. +This can be useful e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.create()` call. {% raw %} ```jsx @@ -225,7 +224,7 @@ The `transform` function can also return a `Promise`, which allows you to do all **Tip**: If you want to have different transformations based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save", and another to "save and notify other admins"), you can set the `transform` prop on [the `<SaveButton>` component](./SaveButton.md), too. -**Tip**: The `transform` function also get the `previousData` in its second argument: +**Tip**: The `transform` function also gets the `previousData` in its second argument: ```jsx export const UseCreate = () => { diff --git a/docs/CreateInDialogButton.md b/docs/CreateInDialogButton.md index 5c4369ba0d5..cd1581568c4 100644 --- a/docs/CreateInDialogButton.md +++ b/docs/CreateInDialogButton.md @@ -81,7 +81,6 @@ In the above example, `<CreateInDialogButton>` is used to create a new employee | -------------- | -------- | ----------------- | ------- | ----------- | | `children` | Required | `ReactNode` | | The content of the dialog. | | `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | -| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | | `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | | `icon` | Optional | `ReactElement` | | Allows to override the default icon. | | `inline` | Optional | `boolean` | | Set to true to display only a Material UI `<IconButton>` instead of the full `<Button>`. | @@ -103,7 +102,6 @@ React-admin provides several built-in form layout components: - [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections - [`LongForm`](./LongForm.md) for long forms with a navigation sidebar - [`WizardForm`](./WizardForm.md) for multi-step forms -- [`CreateDialog`](./CreateDialog.md) for sub-forms in a modal dialog - and [`Form`](./Form.md), a headless component to use as a base for your custom layouts To use an alternative form layout, switch the `<CreateInDialogButton>` child component: @@ -143,20 +141,6 @@ const CreateButton = () => ( ``` {% endraw %} -## `emptyWhileLoading` - -By default, `<CreateInDialogButton>` renders its child component even before the `dataProvider.getOne()` call returns. If you use `<SimpleForm>` or `<TabbedForm>`, this isn't a problem as these components only render when the record has been fetched. But if you use a custom child component that expects the record context to be defined, your component will throw an error. - -To avoid this, set the `emptyWhileLoading` prop to `true`: - -```jsx -const CreateButton = () => ( - <CreateInDialogButton emptyWhileLoading> - ... - </CreateInDialogButton> -); -``` - ## `fullWidth` By default, `<CreateInDialogButton>` renders a [Material UI `<Dialog>`](https://mui.com/material-ui/react-dialog/#full-screen-dialogs) component that takes the width of its content. diff --git a/docs/EditDialog.md b/docs/EditDialog.md index 9dc35b664e8..dad45501c41 100644 --- a/docs/EditDialog.md +++ b/docs/EditDialog.md @@ -255,7 +255,7 @@ The `transform` function can also return a `Promise`, which allows you to do all **Tip**: If you want to have different transformations based on the button clicked by the user (e.g. if the creation form displays two submit buttons, one to "save", and another to "save and notify other admins"), you can set the `transform` prop on [the `<SaveButton>` component](./SaveButton.md), too. -**Tip**: The `transform` function also get the `previousData` in its second argument: +**Tip**: The `transform` function also gets the `previousData` in its second argument: ```jsx export const UserEdit = () => { diff --git a/docs/EditInDialogButton.md b/docs/EditInDialogButton.md index 887358032c3..1f2c3505bf1 100644 --- a/docs/EditInDialogButton.md +++ b/docs/EditInDialogButton.md @@ -74,7 +74,6 @@ const CompanyShow = () => ( | -------------- | -------- | ----------------- | ------- | ----------- | | `children` | Required | `ReactNode` | | The content of the dialog. | | `ButtonProps` | Optional | `object` | | Object containing props to pass to Material UI's `<Button>`. | -| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading. | | `fullWidth` | Optional | `boolean` | `false` | If `true`, the dialog stretches to the full width of the screen. | | `icon` | Optional | `ReactElement` | | Allows to override the default icon. | | `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the record context. | @@ -97,7 +96,6 @@ React-admin provides several built-in form layout components: - [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections - [`LongForm`](./LongForm.md) for long forms with a navigation sidebar - [`WizardForm`](./WizardForm.md) for multi-step forms -- [`EditDialog`](./EditDialog.md) for sub-forms in a modal dialog - and [`Form`](./Form.md), a headless component to use as a base for your custom layouts To use an alternative form layout, switch the `<EditInDialogButton>` child component: @@ -137,20 +135,6 @@ const EditButton = () => ( ``` {% endraw %} -## `emptyWhileLoading` - -By default, `<EditInDialogButton>` renders its child component even before the `dataProvider.getOne()` call returns. If you use `<SimpleForm>` or `<TabbedForm>`, this isn't a problem as these components only render when the record has been fetched. But if you use a custom child component that expects the record context to be defined, your component will throw an error. - -To avoid this, set the `emptyWhileLoading` prop to `true`: - -```jsx -const EditButton = () => ( - <EditInDialogButton emptyWhileLoading> - ... - </EditInDialogButton> -); -``` - ## `fullWidth` By default, `<EditInDialogButton>` renders a [Material UI `<Dialog>`](https://mui.com/material-ui/react-dialog/#full-screen-dialogs) component that takes the width of its content. From 327dd8c7d52139fdccd41b3e32776fcf1db57621 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 29 Nov 2023 18:41:08 +0100 Subject: [PATCH 47/72] fix clearing nested autocompletearrayinput filter --- .../src/list/filter/FilterButton.stories.tsx | 64 +++++++++++++------ .../src/list/filter/FilterForm.spec.tsx | 48 +++++++++++++- .../src/list/filter/FilterForm.tsx | 3 - 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx index 0dd390b90b6..a6b2f6f7d20 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx @@ -20,16 +20,12 @@ export default { title: 'ra-ui-materialui/list/filter/FilterButton', argTypes: { disableSaveQuery: { - control: { - type: 'select', - options: [false, true], - }, + control: 'select', + options: [false, true], }, size: { - control: { - type: 'select', - options: [undefined, 'small', 'medium'], - }, + control: 'select', + options: [undefined, 'small', 'medium'], }, }, }; @@ -198,13 +194,7 @@ export const Basic = (args: { disableSaveQuery?: boolean }) => { source="title" defaultValue="Accusantium qui nihil voluptatum quia voluptas maxime ab similique" />, - <TextInput - label="Nested" - source="nested" - defaultValue={{ foo: 'bar' }} - format={v => v?.foo || ''} - parse={v => ({ foo: v })} - />, + <TextInput label="Nested" source="nested.foo" defaultValue="bar" />, ]; return ( <Admin dataProvider={fakerestDataProvider(data)}> @@ -261,9 +251,30 @@ export const WithSearchInput = (args: { ); }; -export const WithAutoCompleteArrayInput = () => { +const Dashboard = () => <h1>Dashboard</h1>; + +// necessary because fakerest doesn't support nested arrays as filter +const withNestedFiltersSupportDataProvider = () => { + const baseDataprovider = fakerestDataProvider(data); + return { + ...baseDataprovider, + getList: (resource: string, params: any) => { + const newParams = { ...params, filter: { ...params.filter } }; + if (newParams.filter?.nested?.foo) { + newParams.filter['nested.foo'] = newParams.filter.nested.foo; + delete newParams.filter.nested; + } + return baseDataprovider.getList(resource, newParams); + }, + }; +}; + +export const WithAutoCompleteArrayInput = (args: { + disableSaveQuery?: boolean; + size?: 'small' | 'medium'; +}) => { const postFilters: React.ReactElement[] = [ - <SearchInput source="q" alwaysOn />, + <SearchInput source="q" alwaysOn size={args.size} />, <AutocompleteArrayInput label="Title" source="id" @@ -282,18 +293,29 @@ export const WithAutoCompleteArrayInput = () => { { id: 12, name: 'Qui tempore...' }, { id: 13, name: 'Fusce...' }, ]} - alwaysOn - multiple + size={args.size} + />, + <AutocompleteArrayInput + label="Nested" + source="nested.foo" + choices={[ + { id: 'bar', name: 'bar' }, + { id: 'baz', name: 'baz' }, + ]} + size={args.size} />, ]; return ( - <Admin dataProvider={fakerestDataProvider(data)}> + <Admin + dataProvider={withNestedFiltersSupportDataProvider()} + dashboard={Dashboard} + > <Resource name="posts" list={ <PostList postFilters={postFilters} - args={{ disableSaveQuery: false }} + args={{ disableSaveQuery: args.disableSaveQuery }} /> } /> diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx index bd92c31652b..226f162ed73 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx @@ -11,7 +11,7 @@ import * as React from 'react'; import { AdminContext } from '../../AdminContext'; import { ReferenceInput, SelectInput, TextInput } from '../../input'; import { Filter } from './Filter'; -import { Basic } from './FilterButton.stories'; +import { Basic, WithAutoCompleteArrayInput } from './FilterButton.stories'; import { FilterForm, getFilterFormValues, @@ -255,7 +255,7 @@ describe('<FilterForm />', () => { getFilterFormValues(currentFormValues, newFilterValues) ).toEqual({ classicToClear: '', - nestedToClear: '', + nestedToClear: { nestedValue: '' }, classicUpdated: 'ghi2', nestedUpdated: { nestedValue: 'jkl2' }, published_at: '2022-01-01T03:00:00.000Z', @@ -263,4 +263,48 @@ describe('<FilterForm />', () => { }); }); }); + + it('should not reapply previous filter form values when clearing nested AutocompleteArrayInput', async () => { + render(<WithAutoCompleteArrayInput />); + + // Open Posts List + fireEvent.click(await screen.findByText('Posts')); + + // Set nested filter value to 'bar' + fireEvent.click(await screen.findByLabelText('Add filter')); + fireEvent.click( + await screen.findByRole('menuitem', { name: 'Nested' }) + ); + fireEvent.click(await screen.findByText('bar')); + fireEvent.blur(await screen.findByLabelText('Nested')); + await screen.findByText('1-7 of 7'); + expect(screen.queryByRole('button', { name: 'bar' })).not.toBeNull(); + + // Navigate to Dashboard + fireEvent.click(await screen.findByText('Dashboard')); + // Navigate back to Posts List + fireEvent.click(await screen.findByText('Posts')); + // Filter should still be applied + await screen.findByText('1-7 of 7'); + expect(screen.queryByRole('button', { name: 'bar' })).not.toBeNull(); + + // Clear nested filter value + fireEvent.mouseDown( + await screen.findByLabelText('Nested', { selector: 'input' }) + ); + fireEvent.keyDown( + await screen.findByLabelText('Nested', { selector: 'input' }), + { + key: 'Backspace', + } + ); + fireEvent.blur( + await screen.findByLabelText('Nested', { selector: 'input' }) + ); + + // Wait until filter is cleared + await screen.findByText('1-10 of 13'); + // Make sure the 'bar' value is not displayed anymore + expect(screen.queryByRole('button', { name: 'bar' })).toBeNull(); + }, 10000); }); diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx index 6bbba186fd6..90880a86855 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx @@ -285,9 +285,6 @@ const getInputValue = ( innerKey, (filterValues || {})[key] ?? {} ); - if (nestedInputValue === '') { - return acc; - } acc[innerKey] = nestedInputValue; return acc; }, From 3514221e1ef8fd930b4f29f07041a8885379ecdc Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:56:39 +0100 Subject: [PATCH 48/72] Fix EditDialog documentation --- docs/EditDialog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/EditDialog.md b/docs/EditDialog.md index dad45501c41..d2c8941429c 100644 --- a/docs/EditDialog.md +++ b/docs/EditDialog.md @@ -91,7 +91,6 @@ React-admin provides several built-in form layout components: - [`AccordionForm`](./AccordionForm.md) for long forms with collapsible sections - [`LongForm`](./LongForm.md) for long forms with a navigation sidebar - [`WizardForm`](./WizardForm.md) for multi-step forms -- [`EditDialog`](./EditDialog.md) for sub-forms in a modal dialog - and [`Form`](./Form.md), a headless component to use as a base for your custom layouts To use an alternative form layout, switch the `<EditDialog>` child component: From 77c8005c7a6dcb603d7ab9bd73bcc685daeed7b6 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Mon, 4 Dec 2023 14:43:21 +0100 Subject: [PATCH 49/72] fix tests --- .../src/list/filter/FilterButton.spec.tsx | 14 ++++++++++---- .../src/list/filter/FilterButton.stories.tsx | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx index 81ca93fb1b5..3e57d78ca77 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx @@ -188,6 +188,9 @@ describe('<FilterButton />', () => { it('should close the filter menu on removing all filters', async () => { render(<WithAutoCompleteArrayInput />); + // Open Posts List + fireEvent.click(await screen.findByText('Posts')); + await waitFor(() => { expect(screen.queryAllByRole('checkbox')).toHaveLength(11); }); @@ -195,10 +198,13 @@ describe('<FilterButton />', () => { fireEvent.click(screen.getByLabelText('Open')); fireEvent.click(screen.getByText('Sint...')); - await waitFor(() => { - screen.getByLabelText('Add filter'); - expect(screen.getAllByRole('checkbox')).toHaveLength(2); - }); + await waitFor( + () => { + screen.getByLabelText('Add filter'); + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + }, + { timeout: 5000 } + ); fireEvent.click(screen.getByLabelText('Add filter')); fireEvent.click(screen.getByText('Remove all filters')); diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx index a6b2f6f7d20..b2bf3fa622f 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx @@ -294,6 +294,7 @@ export const WithAutoCompleteArrayInput = (args: { { id: 13, name: 'Fusce...' }, ]} size={args.size} + alwaysOn />, <AutocompleteArrayInput label="Nested" From 0b3c7775efc2cb8138099488d7439f1e22c6bb90 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Mon, 4 Dec 2023 15:04:13 +0100 Subject: [PATCH 50/72] increase timeout... =/ --- .../ra-ui-materialui/src/list/filter/FilterButton.spec.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx index 3e57d78ca77..b8a99441622 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx @@ -195,15 +195,14 @@ describe('<FilterButton />', () => { expect(screen.queryAllByRole('checkbox')).toHaveLength(11); }); - fireEvent.click(screen.getByLabelText('Open')); - fireEvent.click(screen.getByText('Sint...')); + fireEvent.click(await screen.findByLabelText('Open')); + fireEvent.click(await screen.findByText('Sint...')); await waitFor( () => { - screen.getByLabelText('Add filter'); expect(screen.getAllByRole('checkbox')).toHaveLength(2); }, - { timeout: 5000 } + { timeout: 10000 } ); fireEvent.click(screen.getByLabelText('Add filter')); From 5a3ce79748ef8ad904d26dff8e60244235a03a7c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:26:07 +0100 Subject: [PATCH 51/72] Update CHANGELOG for v4.16.2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d02c4a51b1f..489ef18c7e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v4.16.2 + +* Fix clearing a nested filter re-renders the previous value when navigating back to the list ([#9491](https://github.com/marmelab/react-admin/pull/9491)) ([slax57](https://github.com/slax57)) +* Fix `ra-data-graphql` uses a Proxy, which prevents adding more methods automatically ([#9487](https://github.com/marmelab/react-admin/pull/9487)) ([fzaninotto](https://github.com/fzaninotto)) +* Fix `useUpdateMany` doesn't accept the `returnPromise` option at call time ([#9486](https://github.com/marmelab/react-admin/pull/9486)) ([fzaninotto](https://github.com/fzaninotto)) +* Fix `<Pagination>` logs a warning ([#9474](https://github.com/marmelab/react-admin/pull/9474)) ([fzaninotto](https://github.com/fzaninotto)) +* [Doc] Update ra-form-layouts dialogs documentation ([#9482](https://github.com/marmelab/react-admin/pull/9482)) ([djhi](https://github.com/djhi)) +* [Doc] Fix snippets fails to render in JS ([#9478](https://github.com/marmelab/react-admin/pull/9478)) ([fzaninotto](https://github.com/fzaninotto)) +* [Doc] Add link to tutorial for headless admin ([#9477](https://github.com/marmelab/react-admin/pull/9477)) ([fzaninotto](https://github.com/fzaninotto)) + ## v4.16.1 * Fix `<FileInput>` should display a validation errors right away when form mode is 'onChange' ([#9459](https://github.com/marmelab/react-admin/pull/9459)) ([slax57](https://github.com/slax57)) From 8a1187da2c25006d924898690e0b03b91041655f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:26:28 +0100 Subject: [PATCH 52/72] v4.16.2 --- examples/data-generator/package.json | 4 ++-- examples/simple/package.json | 14 +++++++------- lerna.json | 2 +- packages/ra-core/package.json | 2 +- packages/ra-data-fakerest/package.json | 4 ++-- packages/ra-data-graphql-simple/package.json | 4 ++-- packages/ra-data-graphql/package.json | 2 +- packages/ra-data-json-server/package.json | 4 ++-- packages/ra-data-localforage/package.json | 4 ++-- packages/ra-data-localstorage/package.json | 4 ++-- packages/ra-data-simple-rest/package.json | 4 ++-- packages/ra-i18n-i18next/package.json | 4 ++-- packages/ra-i18n-polyglot/package.json | 4 ++-- packages/ra-input-rich-text/package.json | 10 +++++----- packages/ra-language-english/package.json | 4 ++-- packages/ra-language-french/package.json | 4 ++-- packages/ra-no-code/package.json | 6 +++--- packages/ra-ui-materialui/package.json | 8 ++++---- packages/react-admin/package.json | 10 +++++----- 19 files changed, 49 insertions(+), 49 deletions(-) diff --git a/examples/data-generator/package.json b/examples/data-generator/package.json index 1170fe1115c..7f63a89bc72 100644 --- a/examples/data-generator/package.json +++ b/examples/data-generator/package.json @@ -1,6 +1,6 @@ { "name": "data-generator-retail", - "version": "4.16.1", + "version": "4.16.2", "homepage": "https://github.com/marmelab/react-admin/tree/master/examples/data-generator", "bugs": "https://github.com/marmelab/react-admin/issues", "license": "MIT", @@ -19,7 +19,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.16.1", + "ra-core": "^4.16.2", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/examples/simple/package.json b/examples/simple/package.json index ee86922d017..1a981690ab6 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -1,6 +1,6 @@ { "name": "simple", - "version": "4.16.1", + "version": "4.16.2", "private": true, "scripts": { "dev": "vite", @@ -17,13 +17,13 @@ "lodash": "~4.17.5", "prop-types": "^15.7.2", "proxy-polyfill": "^0.3.0", - "ra-data-fakerest": "^4.16.1", - "ra-i18n-polyglot": "^4.16.1", - "ra-input-rich-text": "^4.16.1", - "ra-language-english": "^4.16.1", - "ra-language-french": "^4.16.1", + "ra-data-fakerest": "^4.16.2", + "ra-i18n-polyglot": "^4.16.2", + "ra-input-rich-text": "^4.16.2", + "ra-language-english": "^4.16.2", + "ra-language-french": "^4.16.2", "react": "^17.0.0", - "react-admin": "^4.16.1", + "react-admin": "^4.16.2", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", "react-query": "^3.32.1", diff --git a/lerna.json b/lerna.json index e628e515319..d956d982eef 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "lerna": "2.5.1", "packages": ["examples/data-generator", "examples/simple", "packages/*"], - "version": "4.16.1" + "version": "4.16.2" } diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index c0684d17924..30b1dbb6281 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "4.16.1", + "version": "4.16.2", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", diff --git a/packages/ra-data-fakerest/package.json b/packages/ra-data-fakerest/package.json index b0b57af6ad3..24515dcab4b 100644 --- a/packages/ra-data-fakerest/package.json +++ b/packages/ra-data-fakerest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-fakerest", - "version": "4.16.1", + "version": "4.16.2", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -43,7 +43,7 @@ "@types/jest": "^29.5.2", "cross-env": "^5.2.0", "expect": "^27.4.6", - "ra-core": "^4.16.1", + "ra-core": "^4.16.2", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index 423c9200a6f..51d1ce150f6 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql-simple", - "version": "4.16.1", + "version": "4.16.2", "description": "A GraphQL simple data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -37,7 +37,7 @@ "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.17.5", "pluralize": "~7.0.0", - "ra-data-graphql": "^4.16.1" + "ra-data-graphql": "^4.16.2" }, "peerDependencies": { "graphql": "^15.6.0", diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index 6edbdecc9c1..e8a9907c62d 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-graphql", - "version": "4.16.1", + "version": "4.16.2", "description": "A GraphQL data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/ra-data-json-server/package.json b/packages/ra-data-json-server/package.json index d2556fc3a36..0c7ad7d9658 100644 --- a/packages/ra-data-json-server/package.json +++ b/packages/ra-data-json-server/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "4.16.1", + "version": "4.16.2", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "^7.1.1", - "ra-core": "^4.16.1" + "ra-core": "^4.16.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localforage/package.json b/packages/ra-data-localforage/package.json index c16b0592375..96ebbba8c27 100644 --- a/packages/ra-data-localforage/package.json +++ b/packages/ra-data-localforage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-forage", - "version": "4.16.1", + "version": "4.16.2", "description": "LocalForage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -42,7 +42,7 @@ "dependencies": { "localforage": "^1.7.1", "lodash": "~4.17.5", - "ra-data-fakerest": "^4.16.1" + "ra-data-fakerest": "^4.16.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-localstorage/package.json b/packages/ra-data-localstorage/package.json index d62695471f2..2f9337261ea 100644 --- a/packages/ra-data-localstorage/package.json +++ b/packages/ra-data-localstorage/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-local-storage", - "version": "4.16.1", + "version": "4.16.2", "description": "Local storage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -38,7 +38,7 @@ }, "dependencies": { "lodash": "~4.17.5", - "ra-data-fakerest": "^4.16.1" + "ra-data-fakerest": "^4.16.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-simple-rest/package.json b/packages/ra-data-simple-rest/package.json index 6532924e1fa..a877f8c86e4 100644 --- a/packages/ra-data-simple-rest/package.json +++ b/packages/ra-data-simple-rest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "4.16.1", + "version": "4.16.2", "description": "Simple REST data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -29,7 +29,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.16.1", + "ra-core": "^4.16.2", "rimraf": "^3.0.2", "typescript": "^5.1.3" }, diff --git a/packages/ra-i18n-i18next/package.json b/packages/ra-i18n-i18next/package.json index 451151fd5b0..fed53a6b793 100644 --- a/packages/ra-i18n-i18next/package.json +++ b/packages/ra-i18n-i18next/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-i18next", - "version": "4.16.1", + "version": "4.16.2", "description": "i18next i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "i18next": "^23.5.1", - "ra-core": "^4.16.1", + "ra-core": "^4.16.2", "react-i18next": "^13.2.2" }, "devDependencies": { diff --git a/packages/ra-i18n-polyglot/package.json b/packages/ra-i18n-polyglot/package.json index f784e59f0f7..2e40c603cd1 100644 --- a/packages/ra-i18n-polyglot/package.json +++ b/packages/ra-i18n-polyglot/package.json @@ -1,6 +1,6 @@ { "name": "ra-i18n-polyglot", - "version": "4.16.1", + "version": "4.16.2", "description": "Polyglot i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "node-polyglot": "^2.2.2", - "ra-core": "^4.16.1" + "ra-core": "^4.16.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-input-rich-text/package.json b/packages/ra-input-rich-text/package.json index 0502c8c8708..c1bfffab1c7 100644 --- a/packages/ra-input-rich-text/package.json +++ b/packages/ra-input-rich-text/package.json @@ -1,6 +1,6 @@ { "name": "ra-input-rich-text", - "version": "4.16.1", + "version": "4.16.2", "description": "<RichTextInput> component for react-admin, useful for editing HTML code in admin GUIs.", "author": "Gildas Garcia", "repository": "marmelab/react-admin", @@ -51,10 +51,10 @@ "@testing-library/react": "^11.2.3", "@tiptap/extension-mention": "^2.0.3", "@tiptap/suggestion": "^2.0.3", - "data-generator-retail": "^4.16.1", - "ra-core": "^4.16.1", - "ra-data-fakerest": "^4.16.1", - "ra-ui-materialui": "^4.16.1", + "data-generator-retail": "^4.16.2", + "ra-core": "^4.16.2", + "ra-data-fakerest": "^4.16.2", + "ra-ui-materialui": "^4.16.2", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/ra-language-english/package.json b/packages/ra-language-english/package.json index c3b7156af14..8342957c6ae 100644 --- a/packages/ra-language-english/package.json +++ b/packages/ra-language-english/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-english", - "version": "4.16.1", + "version": "4.16.2", "description": "English messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.16.1" + "ra-core": "^4.16.2" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-language-french/package.json b/packages/ra-language-french/package.json index 46634d23c44..a8710bfb2db 100644 --- a/packages/ra-language-french/package.json +++ b/packages/ra-language-french/package.json @@ -1,6 +1,6 @@ { "name": "ra-language-french", - "version": "4.16.1", + "version": "4.16.2", "description": "French messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.16.1" + "ra-core": "^4.16.2" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/packages/ra-no-code/package.json b/packages/ra-no-code/package.json index bed913eaf37..b58d50471f9 100644 --- a/packages/ra-no-code/package.json +++ b/packages/ra-no-code/package.json @@ -1,6 +1,6 @@ { "name": "ra-no-code", - "version": "4.16.1", + "version": "4.16.2", "description": "", "files": [ "*.md", @@ -48,8 +48,8 @@ "lodash": "~4.17.5", "papaparse": "^5.3.0", "prop-types": "^15.6.1", - "ra-data-local-storage": "^4.16.1", - "react-admin": "^4.16.1", + "ra-data-local-storage": "^4.16.2", + "react-admin": "^4.16.2", "react-dropzone": "^12.0.4", "react-query": "^3.32.1" }, diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index b98df63557a..0f2e39e6445 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "4.16.1", + "version": "4.16.2", "description": "UI Components for react-admin with Material UI", "files": [ "*.md", @@ -35,9 +35,9 @@ "file-api": "~0.10.4", "history": "^5.1.0", "ignore-styles": "~5.0.1", - "ra-core": "^4.16.1", - "ra-i18n-polyglot": "^4.16.1", - "ra-language-english": "^4.16.1", + "ra-core": "^4.16.2", + "ra-i18n-polyglot": "^4.16.2", + "ra-language-english": "^4.16.2", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.43.9", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 4111cf5e109..3265ff90910 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "4.16.1", + "version": "4.16.2", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -41,10 +41,10 @@ "@mui/icons-material": "^5.0.1", "@mui/material": "^5.0.2", "history": "^5.1.0", - "ra-core": "^4.16.1", - "ra-i18n-polyglot": "^4.16.1", - "ra-language-english": "^4.16.1", - "ra-ui-materialui": "^4.16.1", + "ra-core": "^4.16.2", + "ra-i18n-polyglot": "^4.16.2", + "ra-language-english": "^4.16.2", + "ra-ui-materialui": "^4.16.2", "react-hook-form": "^7.43.9", "react-router": "^6.1.0", "react-router-dom": "^6.1.0" From d6054fcd8ca974133c689551b0047b6df84e8530 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:27:12 +0100 Subject: [PATCH 53/72] Update yarn.lock for v4.16.2 --- yarn.lock | 82 +++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/yarn.lock b/yarn.lock index b6c06a7b4d9..6a8d8d3eef6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9720,14 +9720,14 @@ __metadata: languageName: node linkType: hard -"data-generator-retail@^4.12.0, data-generator-retail@^4.16.1, data-generator-retail@workspace:examples/data-generator": +"data-generator-retail@^4.12.0, data-generator-retail@^4.16.2, data-generator-retail@workspace:examples/data-generator": version: 0.0.0-use.local resolution: "data-generator-retail@workspace:examples/data-generator" dependencies: cross-env: ^5.2.0 date-fns: ^2.19.0 faker: ^4.1.0 - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18086,7 +18086,7 @@ __metadata: languageName: node linkType: hard -"ra-core@^4.16.1, ra-core@workspace:packages/ra-core": +"ra-core@^4.16.2, ra-core@workspace:packages/ra-core": version: 0.0.0-use.local resolution: "ra-core@workspace:packages/ra-core" dependencies: @@ -18132,7 +18132,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.16.1, ra-data-fakerest@workspace:packages/ra-data-fakerest": +"ra-data-fakerest@^4.12.0, ra-data-fakerest@^4.16.2, ra-data-fakerest@workspace:packages/ra-data-fakerest": version: 0.0.0-use.local resolution: "ra-data-fakerest@workspace:packages/ra-data-fakerest" dependencies: @@ -18140,7 +18140,7 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 fakerest: ^3.0.0 - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18158,7 +18158,7 @@ __metadata: graphql-ast-types-browser: ~1.0.2 lodash: ~4.17.5 pluralize: ~7.0.0 - ra-data-graphql: ^4.16.1 + ra-data-graphql: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18167,7 +18167,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-graphql@^4.12.0, ra-data-graphql@^4.16.1, ra-data-graphql@workspace:packages/ra-data-graphql": +"ra-data-graphql@^4.12.0, ra-data-graphql@^4.16.2, ra-data-graphql@workspace:packages/ra-data-graphql": version: 0.0.0-use.local resolution: "ra-data-graphql@workspace:packages/ra-data-graphql" dependencies: @@ -18190,7 +18190,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18203,7 +18203,7 @@ __metadata: cross-env: ^5.2.0 localforage: ^1.7.1 lodash: ~4.17.5 - ra-data-fakerest: ^4.16.1 + ra-data-fakerest: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18211,13 +18211,13 @@ __metadata: languageName: unknown linkType: soft -"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.16.1, ra-data-local-storage@workspace:packages/ra-data-localstorage": +"ra-data-local-storage@^4.12.0, ra-data-local-storage@^4.16.2, ra-data-local-storage@workspace:packages/ra-data-localstorage": version: 0.0.0-use.local resolution: "ra-data-local-storage@workspace:packages/ra-data-localstorage" dependencies: cross-env: ^5.2.0 lodash: ~4.17.5 - ra-data-fakerest: ^4.16.1 + ra-data-fakerest: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18231,7 +18231,7 @@ __metadata: dependencies: cross-env: ^5.2.0 query-string: ^7.1.1 - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 peerDependencies: @@ -18246,26 +18246,26 @@ __metadata: cross-env: ^5.2.0 i18next: ^23.5.1 i18next-resources-to-backend: ^1.1.4 - ra-core: ^4.16.1 + ra-core: ^4.16.2 react-i18next: ^13.2.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.16.1, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": +"ra-i18n-polyglot@^4.12.0, ra-i18n-polyglot@^4.16.2, ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot": version: 0.0.0-use.local resolution: "ra-i18n-polyglot@workspace:packages/ra-i18n-polyglot" dependencies: cross-env: ^5.2.0 node-polyglot: ^2.2.2 - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.16.1, ra-input-rich-text@workspace:packages/ra-input-rich-text": +"ra-input-rich-text@^4.12.0, ra-input-rich-text@^4.16.2, ra-input-rich-text@workspace:packages/ra-input-rich-text": version: 0.0.0-use.local resolution: "ra-input-rich-text@workspace:packages/ra-input-rich-text" dependencies: @@ -18287,10 +18287,10 @@ __metadata: "@tiptap/starter-kit": ^2.0.3 "@tiptap/suggestion": ^2.0.3 clsx: ^1.1.1 - data-generator-retail: ^4.16.1 - ra-core: ^4.16.1 - ra-data-fakerest: ^4.16.1 - ra-ui-materialui: ^4.16.1 + data-generator-retail: ^4.16.2 + ra-core: ^4.16.2 + ra-data-fakerest: ^4.16.2 + ra-ui-materialui: ^4.16.2 react: ^17.0.0 react-dom: ^17.0.0 react-hook-form: ^7.43.9 @@ -18307,21 +18307,21 @@ __metadata: languageName: unknown linkType: soft -"ra-language-english@^4.12.0, ra-language-english@^4.16.1, ra-language-english@workspace:packages/ra-language-english": +"ra-language-english@^4.12.0, ra-language-english@^4.16.2, ra-language-english@workspace:packages/ra-language-english": version: 0.0.0-use.local resolution: "ra-language-english@workspace:packages/ra-language-english" dependencies: - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown linkType: soft -"ra-language-french@^4.12.0, ra-language-french@^4.16.1, ra-language-french@workspace:packages/ra-language-french": +"ra-language-french@^4.12.0, ra-language-french@^4.16.2, ra-language-french@workspace:packages/ra-language-french": version: 0.0.0-use.local resolution: "ra-language-french@workspace:packages/ra-language-french" dependencies: - ra-core: ^4.16.1 + ra-core: ^4.16.2 rimraf: ^3.0.2 typescript: ^5.1.3 languageName: unknown @@ -18342,9 +18342,9 @@ __metadata: lodash: ~4.17.5 papaparse: ^5.3.0 prop-types: ^15.6.1 - ra-data-local-storage: ^4.16.1 + ra-data-local-storage: ^4.16.2 react: ^17.0.0 - react-admin: ^4.16.1 + react-admin: ^4.16.2 react-dom: ^17.0.0 react-dropzone: ^12.0.4 react-query: ^3.32.1 @@ -18358,7 +18358,7 @@ __metadata: languageName: unknown linkType: soft -"ra-ui-materialui@^4.16.1, ra-ui-materialui@workspace:packages/ra-ui-materialui": +"ra-ui-materialui@^4.16.2, ra-ui-materialui@workspace:packages/ra-ui-materialui": version: 0.0.0-use.local resolution: "ra-ui-materialui@workspace:packages/ra-ui-materialui" dependencies: @@ -18381,9 +18381,9 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.0 query-string: ^7.1.1 - ra-core: ^4.16.1 - ra-i18n-polyglot: ^4.16.1 - ra-language-english: ^4.16.1 + ra-core: ^4.16.2 + ra-i18n-polyglot: ^4.16.2 + ra-language-english: ^4.16.2 react: ^17.0.0 react-dom: ^17.0.0 react-dropzone: ^12.0.4 @@ -18561,7 +18561,7 @@ __metadata: languageName: unknown linkType: soft -"react-admin@^4.12.0, react-admin@^4.16.1, react-admin@workspace:packages/react-admin": +"react-admin@^4.12.0, react-admin@^4.16.2, react-admin@workspace:packages/react-admin": version: 0.0.0-use.local resolution: "react-admin@workspace:packages/react-admin" dependencies: @@ -18572,10 +18572,10 @@ __metadata: cross-env: ^5.2.0 expect: ^27.4.6 history: ^5.1.0 - ra-core: ^4.16.1 - ra-i18n-polyglot: ^4.16.1 - ra-language-english: ^4.16.1 - ra-ui-materialui: ^4.16.1 + ra-core: ^4.16.2 + ra-i18n-polyglot: ^4.16.2 + ra-language-english: ^4.16.2 + ra-ui-materialui: ^4.16.2 react-hook-form: ^7.43.9 react-router: ^6.1.0 react-router-dom: ^6.1.0 @@ -20135,13 +20135,13 @@ __metadata: lodash: ~4.17.5 prop-types: ^15.7.2 proxy-polyfill: ^0.3.0 - ra-data-fakerest: ^4.16.1 - ra-i18n-polyglot: ^4.16.1 - ra-input-rich-text: ^4.16.1 - ra-language-english: ^4.16.1 - ra-language-french: ^4.16.1 + ra-data-fakerest: ^4.16.2 + ra-i18n-polyglot: ^4.16.2 + ra-input-rich-text: ^4.16.2 + ra-language-english: ^4.16.2 + ra-language-french: ^4.16.2 react: ^17.0.0 - react-admin: ^4.16.1 + react-admin: ^4.16.2 react-app-polyfill: ^1.0.4 react-dom: ^17.0.0 react-hook-form: ^7.43.9 From 0e78e4040efc51f8211f574fd1bc99d1cd23aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Mon, 4 Dec 2023 18:40:46 +0100 Subject: [PATCH 54/72] Update list of Enterprise Edition modules --- docs/Readme.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/Readme.md b/docs/Readme.md index 3b383792cd1..537eb22921b 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -135,15 +135,21 @@ Read more about the [Architecture choices](./Architecture.md). The [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise) <img class="icon" src="./img/premium.svg" /> offers additional features and services for react-admin: - Save weeks of development thanks to the **Private Modules**, valid on an unlimited number of domains and projects. - - `ra-preferences`: Persist user preferences (language, theme, filters, datagrid columns, sidebar position, etc.) in local storage. - - `ra-navigation`: Multi-level menu and breadcrumb, with the ability to define a custom path for your resources. - - `ra-realtime`: Display live notifications, auto-update content on the screen, lock content when editing, with adapters for real-time backends. + - `ra-ai`: Components powered by Artificial Intelligence (AI) to boost user productivity. Suggest completion for user inputs, fix and improve large chunks of text in React-Admin forms. + - `ra-audit-log`: Track all changes made to your data, and display them in a dedicated view. + - `ra-calendar`: Display and manipulate events, drag and resize appointments, and browse a calendar in react-admin apps. + - `ra-datagrid-ag`: Integration with the [ag-Grid](https://www.ag-grid.com/) data grid, for better performance and advanced features (row grouping, aggregation, tree data, pivoting, column resizing, and much more). - `ra-editable-datagrid`: Edit data directly in the list view, for better productivity. Excel-like editing experience. - `ra-form-layout`: New form layouts for complex data entry tasks (accordion, wizard, etc.) + - `ra-json-schema-form`: Generate react-admin apps from a JSON Schema. + - `ra-markdown`: Read Markdown data, and edit it using a WYSIWYG editor in your admin + - `ra-navigation`: Alternative layouts and menus, breadcrumb, and hooks for applications with a deep navigation tree. + - `ra-rbac`: Role-based access control for fine-grained permissions. + - `ra-realtime`: Display live notifications, auto-update content on the screen, lock content when editing, with adapters for real-time backends. - `ra-relationships`: Visualize and edit complex relationships, including many-to-many relationships. - - `ra-tree`: Edit and visualize tree structures. Reorganize by drag and drop. Adapts to any data structure on the backend (parent_id, children, nested sets, etc.). + - `ra-search`: Plug your search engine and let users search across all resources via a smart Omnibox. - `ra-tour`: Guided tours for react-admin applications. Step-by-step instructions, Material UI skin. - - `ra-markdown`: Read Markdown data, and edit it using a WYSIWYG editor in your admin + - `ra-tree`: Edit and visualize tree structures. Reorganize by drag and drop. Adapts to any data structure on the backend (parent_id, children, nested sets, etc.). - Get **Support** from experienced react and react-admin developers, who will help you find the right information and troubleshoot your bugs. - Get a **50% Discount on Professional Services** in case you need coaching, audit, or custom development by our experts. - Get access to exclusive **Learning Material**, including a Storybook full of examples, and a dedicated demo app. From d21293711fe27fed640d867ee8f5d09818923474 Mon Sep 17 00:00:00 2001 From: adrien guernier <adrien@marmelab.com> Date: Wed, 6 Dec 2023 12:22:17 +0100 Subject: [PATCH 55/72] doc: improve EditableDatagrid doc --- docs/EditableDatagrid.md | 609 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 601 insertions(+), 8 deletions(-) diff --git a/docs/EditableDatagrid.md b/docs/EditableDatagrid.md index 7eb3f223821..03bead679da 100644 --- a/docs/EditableDatagrid.md +++ b/docs/EditableDatagrid.md @@ -5,7 +5,9 @@ title: "The EditableDatagrid Component" # `<EditableDatagrid>` -This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component offers an "edit-in-place" experience in a `<Datagrid>`. +The default react-admin user-experience consists of three pages: List, Edit, and Create. However, in some cases, users may prefer to do all search and edition tasks in one page. + +This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component offers an "edit-in-place" experience, that allows users to edit, create, and delete records in place inside a `<Datagrid>`. <video controls autoplay playsinline muted loop> <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-editable-datagrid-overview.webm" type="video/webm" /> @@ -13,6 +15,10 @@ This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" s Your browser does not support the video tag. </video> +With `<EditableDatagrid>`, users can click on a row in the datagrid to replace the row with an edition form, and edit the corresponding record without leaving the list. They can also create new records by clicking on the Create button, which inserts an empty editable row as the first line of the list. Finally, they can delete a record by clicking on the Delete button on each row. + +You can test it live in [the Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-editabledatagrid--undoable) and in [the e-commerce demo](https://marmelab.com/ra-enterprise-demo/#/tours/ra-editable-datagrid). + ## Usage First, install the `@react-admin/ra-editable-datagrid` package: @@ -23,11 +29,12 @@ npm install --save @react-admin/ra-editable-datagrid yarn add @react-admin/ra-editable-datagrid ``` -**Tip**: `ra-editable-datagrid` is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package. +**Tip**: `ra-editable-datagrid` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. -Then, use `<EditableDatagrid>` as a child of a react-admin `<List>`, `<ReferenceManyField>`, or any other component that creates a `ListContext`. +Then, use `<EditableDatagrid>` in replacement of a `<Datagrid>`, as a child of a react-admin `<List>`, `<ReferenceManyField>` or any other component that creates a `ListContext`. In addition, pass a form component to be displayed when the user switches to edit or create mode. -```jsx +{% raw %} +```tsx import { List, TextField, @@ -46,7 +53,21 @@ const professionChoices = [ { id: 'other', name: 'Other' }, ]; -const ArtistList = () => ( +const ArtistForm = () => ( + <RowForm> + <TextField source="id" /> + <TextInput source="firstname" validate={required()} /> + <TextInput source="name" validate={required()} /> + <DateInput source="dob" label="born" validate={required()} /> + <SelectInput + source="prof" + label="Profession" + choices={professionChoices} + /> + </RowForm> +); + +export const ArtistList = () => ( <List hasCreate empty={false}> <EditableDatagrid mutationMode="undoable" @@ -65,13 +86,447 @@ const ArtistList = () => ( </EditableDatagrid> </List> ); +``` +{% endraw %} + + +## Props + +| Prop | Required | Type | Default | Description | +| -------------- | -------- | ------------ | ---------- | ------------------------------------------------------------------------- | +| `editForm` | Required | ReactElement | - | The component to display instead of a row when the users edit a record. | +| `actions` | Optional | ReactElement | - | The component used to customize the actions buttons. | +| `createForm` | Optional | ReactElement | - | The component to display as the first row when the user creates a record. | +| `mutationMode` | Optional | `string` | `undoable` | Mutation mode (`'undoable'`, `'pessimistic'` or `'optimistic'`). | +| `noDelete` | Optional | boolean | - | Disable the inline Delete button. | + +`<EditableDatagrid>` is a drop-in replacement for `<Datagrid>` it means that you can provide additional props to passed them down to [`<Datagrid>` component](./Datagrid.md#props). + +## `editForm` + +The component to display instead of a row when the users edit a record. It renders as many columns as the `<EditableDatagrid>` has children. + +The `<EditableDatagrid>` component renders the `editForm` elements in a `<table>`, so these elements should render a `<tr>`. We advise you to use the [`<RowForm>`](#rowform) component for `editForm`, which renders a `<tr>` by default. But you can also use your own component to render the row form ([see below](#rowform)). + +```tsx +const ArtistForm = () => ( + <RowForm> + <TextField source="id" /> + {/*...*/} + </RowForm> +); + +export const ArtistList = () => ( + <List> + <EditableDatagrid editForm={<ArtistForm />}> + <TextField source="id" /> + {/*...*/} + </EditableDatagrid> + </List> +); +``` + +**Tip**: No need to include an `<EditButton>` as child, the `<EditableDatagrid>` component adds a column with edit/delete/save/cancel buttons itself. + +## `actions` + +By default, the `<EditableDatagrid>` will show both edit and delete buttons when users hover a row. If you want to either customize the buttons behavior or provide more actions, you can leverage the `actions` prop, which accepts a React element. For instance, here's how to customize the delete button so that it asks users for a confirmation but still allows to undo the deletion: + +{% raw %} +```tsx +import React from 'react'; +import { List, TextField } from 'react-admin'; +import { + DeleteWithConfirmIconButton, + EditableDatagrid, + EditRowButton, +} from '@react-admin/ra-editable-datagrid'; +import { ArtistForm } from './ArtistForm'; + +const CustomAction = () => ( + <> + <EditRowButton /> + <DeleteWithConfirmIconButton mutationMode="undoable" /> + </> +); + +export const ArtistList = () => ( + <List hasCreate sort={{ field: 'id', order: 'DESC' }} empty={false}> + <EditableDatagrid + actions={<CustomAction />} + // The mutation mode is still applied to updates + mutationMode="undoable" + editForm={<ArtistForm />} + > + <TextField source="id" /> + <TextField source="firstname" /> + <TextField source="name" /> + </EditableDatagrid> + </List> +); +``` +{% endraw %} + +## `createForm` + +The component to display as the first row when the user creates a record. + +The `<EditableDatagrid>` component renders the `createForm` elements in a `<table>`, so these elements should render a `<tr>`. We advise you to use the [`<RowForm>`](#rowform) component for `createForm`, which renders a `<tr>` by default. But you can also use your own component to render the row form ([see below](#rowform)) + +To display a create button on top of the list, you should add the [`hasCreate`](./List.md#hascreate) prop to the `<List>` component, as in the example below. +```tsx const ArtistForm = () => ( <RowForm> + <TextField source="id" /> + {/*...*/} + </RowForm> +); + +export const ArtistList = () => ( + <List hasCreate> + <EditableDatagrid createForm={<ArtistForm />}> + <TextField source="id" /> + {/*...*/} + </EditableDatagrid> + </List> +); +``` + +To be able to add a new row when the list is empty, you need to bypass the default `<List>` empty page system by passing `empty={false}` as `<List>` prop. + +```tsx +export const ArtistList = () => ( + <List hasCreate empty={false}> + <EditableDatagrid createForm={<ArtistForm />}> + {/*...*/} + </EditableDatagrid> + </List> +); +``` + +**Tip**: To display a custom create button, pass a custom component as the [`empty`](./Datagrid.md#empty) `Datagrid>` prop. It can use the [`useEditableDatagridContext`](#useeditabledatagridcontext) hook that allows you to manage the visibility of the creation form. + +```tsx +const MyCreateButton = () => ( + <Box> + <Typography>No books yet</Typography> + <Typography>Do you want to add one?</Typography> + <CreateButton label="Create the first book" /> + </Box> +); + +export const ArtistList = () => ( + <List hasCreate empty={false}> + <EditableDatagrid createForm={<ArtistForm />} empty={<MyCreateButton />}> + {/*...*/} + </EditableDatagrid> + </List> +); +``` + +Feel free to visit the [dedicated stories](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-empty--custom-empty-in-list) to see more examples. + +## `mutationMode` + +Use the `mutationMode` prop to specify the [mutation mode](./Edit.html#mutationmode). By default, the `<EditableDatagrid>` uses the `undoable` mutation mode. + +```jsx +<EditableDatagrid mutationMode="undoable"> + {/*...*/} +</EditableDatagrid> +``` + +## `noDelete` + +You can disable the delete button by setting the `noDelete` prop to `true`: + +```jsx +<EditableDatagrid noDelete={true}> + {/*...*/} +</EditableDatagrid> +``` + +## `<RowForm>` + +`<RowForm>` renders a table row with one cell per child. That means that `<RowForm>` and `<EditableDatagrid>` should have the same number of children, and these children should concern the same `source`. + +If you want to avoid the edition of a column, use a `<Field>` component instead of an `<Input>` component (like the `<TextField>` in the example above). + +`<RowForm>` accepts the following props: + +| Prop | Required | Type | Default | Description | +| ----------------- | -------- | ------------ | ------- | ------------------------------------------------------------------------- | +| `mutationOptions` | Optional | `Object` | - | An object that can contain `onSuccess` and `onError` functions to be executed after the row has been saved and after the row has failed to be saved respectively. | +| `submitOnEnter` | Optional | `function` | - | A function to transform the row before it is saved. | +| `transform` | Optional | `boolean` | `true` | Whether the form can be submitted by pressing the Enter key. | + +Any additional props passed to `<RowForm>` are passed to the underlying react-admin [`<Form>`](./Form.md) component. That means that you can pass e.g. `defaultValues`, or `validate` props. + +{% raw %} +```tsx +import { RowForm } from '@react-admin/ra-editable-datagrid'; + +const ArtistForm = () => ( + <RowForm defaultValues={{ firstname: 'John', name: 'Doe' }}> + <TextField source="id" disabled /> + <TextInput source="name" validate={required()} /> + </RowForm> +); +``` +{% endraw %} + +## `useEditableDatagridContext` + +For advanced use cases, you can use the `useEditableDatagridContext` hook to manage the visibility of the creation form. It returns the following callbacks: + +- `openStandaloneCreateForm`: A function to open the create form. +- `closeStandaloneCreateForm`: A function to close the create form. + +```tsx +import React from 'react'; +import { Typography, Box } from '@mui/material'; +import { CreateButton, List } from 'react-admin'; +import { + EditableDatagrid, + useEditableDatagridContext, +} from '@react-admin/ra-editable-datagrid'; + +const MyCreateButton = () => { + const { openStandaloneCreateForm } = useEditableDatagridContext(); + const handleClick = () => { + openStandaloneCreateForm(); + }; + return ( + <Box> + <Typography>No books yet</Typography> + <Typography>Do you want to add one?</Typography> + <CreateButton onClick={handleClick} label="Create the first book" /> + </Box> + ); +}; + +export const BookList = () => ( + <List hasCreate empty={false}> + <EditableDatagrid empty={<MyCreateButton />}> + {/*...*/} + </EditableDatagrid> + </List> +); +``` + +Feel free to visit the [dedicated stories](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-empty--custom-empty-standalone) to see more examples. + +## Recipes + +### Inside A `<ReferenceManyField>` + +Here is another example inside a `<ReferenceManyField>`. The only difference with its usage in a `<List>` is that you have to initialize the foreign key in the create form using the `defaultValues` prop: + +{% raw %} +```tsx +import React from 'react'; +import { + DateField, + DateInput, + Edit, + NumberField, + NumberInput, + ReferenceManyField, + required, + SimpleForm, + TextField, + TextInput, +} from 'react-admin'; +import { useFormContext } from 'react-hook-form'; +import { EditableDatagrid, RowForm } from '@react-admin/ra-editable-datagrid'; + +const OrderEdit = () => ( + <Edit> + <SimpleForm> + <ReferenceManyField + fullWidth + label="Products" + reference="products" + target="order_id" + > + <EditableDatagrid + mutationMode="undoable" + createForm={<ProductForm />} + editForm={<ProductForm />} + rowClick="edit" + > + <TextField source="id" /> + <TextField source="name" /> + <NumberField source="price" label="Default Price" /> + <DateField source="available_since" /> + </EditableDatagrid> + </ReferenceManyField> + <DateInput source="purchase_date" /> + </SimpleForm> + </Edit> +); + +const ProductForm = () => { + const { getValues } = useFormContext(); + return ( + <RowForm defaultValues={{ order_id: getValues('id') }}> + <TextInput source="id" disabled /> + <TextInput source="name" validate={required()} /> + <NumberInput + source="price" + label="Default Price" + validate={required()} + /> + <DateInput source="available_since" validate={required()} /> + </RowForm> + ); +}; +``` +{% endraw %} + +In these examples, the same form component is used in `createForm` and `editForm`, but you can pass different forms (e.g. if some fields can be set at creation but not changed afterwards). + +### Providing Custom Side Effects + +Like other forms in react-admin, you can provide your own side effects in response to successful or failed actions by passing functions to the `onSuccess` or `onError` inside the `mutationOptions` prop: + +{% raw %} +```tsx +import { TextInput, DateInput, SelectInput, useNotify } from 'react-admin'; +import { RowForm, useRowContext } from '@react-admin/ra-editable-datagrid'; + +const ArtistEditForm = () => { + const notify = useNotify(); + const { close } = useRowContext(); + + const handleSuccess = response => { + notify( + `Artist ${response.name} ${response.firstName} has been updated` + ); + close(); + }; + + return ( + <RowForm mutationOptions={{ onSuccess: handleSuccess }}> + <TextField source="id" /> + <TextInput source="firstname" validate={required()} /> + <TextInput source="name" validate={required()} /> + <DateInput source="dob" label="born" validate={required()} /> + <SelectInput + source="prof" + label="Profession" + choices={professionChoices} + /> + </RowForm> + ); +}; + +const ArtistCreateForm = () => { + const notify = useNotify(); + const { close } = useRowContext(); + + const handleSuccess = response => { + notify(`Artist ${response.name} ${response.firstName} has been added`); + close(); + }; + + return ( + <RowForm mutationOptions={{ onSuccess: handleSuccess }}> + <TextInput source="firstname" validate={required()} /> + <TextInput source="name" validate={required()} /> + <DateInput source="dob" label="born" validate={required()} /> + <SelectInput + source="prof" + label="Profession" + choices={professionChoices} + /> + </RowForm> + ); +}; +``` +{% endraw %} + +Note that we provide an additional side effects hook: `useRowContext` which allows you to close the form. + +**Tip**: If you use `useNotify` inside an `onSuccess` side effect for an Edit form in addition to the `<EditableDatagrid mutationMode="undoable">` prop, you will need to set the notification as undoable for the changes to take effects. Also, note that, on undoable forms, the `onSuccess` side effect will be called immediately, without any `response` argument. + +```ts +const handleSuccess = () => { + notify('Artist has been updated', { type: 'info', undoable: true }); + close(); +}; +``` + +Besides, the `<RowForm>` also accept a function for its `transform` prop allowing you to alter the data before sending it to the dataProvider: + +```tsx +import { TextInput, DateInput, SelectInput } from 'react-admin'; +import { RowForm } from '@react-admin/ra-editable-datagrid'; + +const ArtistCreateForm = () => { + const handleTransform = data => { + return { + ...data, + fullName: `${data.firstName} ${data.name}`, + }; + }; + + return ( + <RowForm transform={handleTransform}> + <TextInput source="firstname" validate={required()} /> + <TextInput source="name" validate={required()} /> + <DateInput source="dob" label="born" validate={required()} /> + <SelectInput + source="prof" + label="Profession" + choices={professionChoices} + /> + </RowForm> + ); +}; +``` + +### Adding A `meta` Prop To All Mutations + +Just like with `<Datagrid>`, if you'd like to add a `meta` prop to all the dataProvider calls, you will need to provide custom `mutationOptions` at all the places where mutations occur: + +- the `createForm` +- the `editForm` +- the `<DeleteRowButton>` + +Here is a complete example: + +{% raw %} +```tsx +import * as React from 'react'; +import { + TextInput, + DateInput, + SelectInput, + TextField, + DateField, + SelectField, + required, + List, +} from 'react-admin'; +import { + EditableDatagrid, + RowForm, + RowFormProps, + EditRowButton, + DeleteRowButton, +} from '@react-admin/ra-editable-datagrid'; + +const ArtistForm = ({ meta }) => ( + <RowForm + defaultValues={{ firstname: 'John', name: 'Doe' }} + mutationOptions={{ meta }} + > <TextField source="id" /> <TextInput source="firstname" validate={required()} /> <TextInput source="name" validate={required()} /> - <DateInput source="dob" label="born" validate={required()} /> + <DateInput source="dob" label="Born" validate={required()} /> <SelectInput source="prof" label="Profession" @@ -79,9 +534,147 @@ const ArtistForm = () => ( /> </RowForm> ); + +const ArtistListWithMeta = () => { + const meta = { foo: 'bar' }; + return ( + <List hasCreate sort={{ field: 'id', order: 'DESC' }} empty={false}> + <EditableDatagrid + createForm={<ArtistForm meta={meta} />} + editForm={<ArtistForm meta={meta} />} + rowClick="edit" + actions={ + <> + <EditRowButton /> + <DeleteRowButton mutationOptions={{ meta }} /> + </> + } + > + <TextField source="id" /> + <TextField source="firstname" /> + <TextField source="name" /> + <DateField source="dob" label="Born" /> + <SelectField + source="prof" + label="Profession" + choices={professionChoices} + /> + </EditableDatagrid> + </List> + ); +}; ``` +{% endraw %} + +### Configurable -`<EditableDatagrid>` is a drop-in replacement for `<Datagrid>`. It expects 2 additional props: `createForm` and `editForm`, the components to be displayed when a user creates or edits a row. The `<RowForm>` component allows to create such forms using react-admin Input components. +You can let end users customize what fields are displayed in the `<EditableDatagrid>` by using the `<EditableDatagridConfigurable>` component instead, together with the `<RowFormConfigurable>` component. +<video controls autoplay playsinline muted loop> + <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-editable-datagrid-configurable.mp4" type="video/mp4"/> + <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-editable-datagrid-configurable.webm" type="video/webm"/> + Your browser does not support the video tag. +</video> + +```diff +import { List, TextField } from 'react-admin'; +import { +- EditableDatagrid, ++ EditableDatagridConfigurable, +- RowForm, ++ RowFormConfigurable, +} from '@react-admin/ra-editable-datagrid'; + +const ArtistForm = ({ meta }) => ( +- <RowForm> ++ <RowFormConfigurable> + <TextField source="id" /> + <TextInput source="firstname" validate={required()} /> + <TextInput source="name" validate={required()} /> + <DateInput source="dob" label="Born" validate={required()} /> + <SelectInput + source="prof" + label="Profession" + choices={professionChoices} + /> +- </RowForm> ++ </RowFormConfigurable> +); + +const ArtistList = () => ( + <List hasCreate empty={false}> +- <EditableDatagrid ++ <EditableDatagridConfigurable + mutationMode="undoable" + createForm={<ArtistForm />} + editForm={<ArtistForm />} + > + <TextField source="id" /> + <TextField source="firstname" /> + <TextField source="name" /> + <DateField source="dob" label="born" /> + <SelectField + source="prof" + label="Profession" + choices={professionChoices} + /> +- </EditableDatagrid> ++ </EditableDatagridConfigurable> + </List> +); +``` + +When users enter the configuration mode and select the `<EditableDatagrid>`, they can show / hide datagrid columns. They can also use the [`<SelectColumnsButton>`](./SelectColumnsButton.md) + +By default, `<EditableDatagridConfigurable>` renders all child fields. But you can also omit some of them by passing an `omit` prop containing an array of field sources: + +```tsx +// by default, hide the id and author columns +// users can choose to show them in configuration mode +const PostList = () => ( + <List> + <EditableDatagridConfigurable omit={['id', 'author']}> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + </EditableDatagridConfigurable> + </List> +); +``` + +If you render more than one `<EditableDatagridConfigurable>` in the same page, you must pass a unique `preferenceKey` prop to each one: + +```tsx +const PostList = () => ( + <List> + <EditableDatagridConfigurable preferenceKey="posts.datagrid"> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + </EditableDatagridConfigurable> + </List> +); +``` + +The inspector uses the field `source` (or `label` when it's a string) to display the column name. If you use non-field children (e.g. action buttons), then it's your responsibility to wrap them in a component with a `label` prop, that will be used by the inspector: + +```tsx +const FieldWrapper = ({ children, label }) => children; +const PostList = () => ( + <List> + <EditableDatagridConfigurable> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + <FieldWrapper label="Actions"> + <EditButton /> + </FieldWrapper> + </EditableDatagridConfigurable> + </List> +); +``` -Check [the `ra-editable-datagrid` documentation](https://marmelab.com/ra-enterprise/modules/ra-editable-datagrid) for more details. +`<EditableDatagridConfigurable>` accepts the same props as `<EditableDatagrid>`. \ No newline at end of file From 2f874053ecb219b9041d18af1b76b1084156f542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= <fzaninotto@gmail.com> Date: Wed, 6 Dec 2023 16:09:16 +0100 Subject: [PATCH 56/72] Review --- docs/EditableDatagrid.md | 332 ++++++++++++++++++--------------------- 1 file changed, 155 insertions(+), 177 deletions(-) diff --git a/docs/EditableDatagrid.md b/docs/EditableDatagrid.md index 03bead679da..a6dc52e188b 100644 --- a/docs/EditableDatagrid.md +++ b/docs/EditableDatagrid.md @@ -5,9 +5,9 @@ title: "The EditableDatagrid Component" # `<EditableDatagrid>` -The default react-admin user-experience consists of three pages: List, Edit, and Create. However, in some cases, users may prefer to do all search and edition tasks in one page. +The default react-admin user experience consists of three pages: List, Edit, and Create. However, in some cases, users may prefer to do all CRUD tasks in one page. -This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component offers an "edit-in-place" experience, that allows users to edit, create, and delete records in place inside a `<Datagrid>`. +`<EditableDatagrid>` is an [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component that offers an "edit-in-place" experience, allowing users to edit, create, and delete records in place inside a `<Datagrid>`. <video controls autoplay playsinline muted loop> <source src="https://marmelab.com/ra-enterprise/modules/assets/ra-editable-datagrid-overview.webm" type="video/webm" /> @@ -15,9 +15,11 @@ This [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" s Your browser does not support the video tag. </video> -With `<EditableDatagrid>`, users can click on a row in the datagrid to replace the row with an edition form, and edit the corresponding record without leaving the list. They can also create new records by clicking on the Create button, which inserts an empty editable row as the first line of the list. Finally, they can delete a record by clicking on the Delete button on each row. +With `<EditableDatagrid>`, when users click on a row in the datagrid, the row content is replaced by the edition form. They can also create new records by clicking on the Create button, which inserts an empty editable row as the first line of the list. Finally, they can delete a record by clicking on the Delete button on each row. -You can test it live in [the Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-editabledatagrid--undoable) and in [the e-commerce demo](https://marmelab.com/ra-enterprise-demo/#/tours/ra-editable-datagrid). +You can test it live in [the Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-editabledatagrid--undoable) and [the e-commerce demo](https://marmelab.com/ra-enterprise-demo/#/tours/ra-editable-datagrid). + +`<EditableDatagrid>` allows you to use any [Input component](./ImageInput.md) to edit the record - including Reference Inputs for foreign keys. ## Usage @@ -29,69 +31,52 @@ npm install --save @react-admin/ra-editable-datagrid yarn add @react-admin/ra-editable-datagrid ``` -**Tip**: `ra-editable-datagrid` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +**Tip**: `ra-editable-datagrid` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this registry. -Then, use `<EditableDatagrid>` in replacement of a `<Datagrid>`, as a child of a react-admin `<List>`, `<ReferenceManyField>` or any other component that creates a `ListContext`. In addition, pass a form component to be displayed when the user switches to edit or create mode. +Then, replace `<Datagrid>` with `<EditableDatagrid>` in a react-admin `<List>`, `<ReferenceManyField>`, or any other component that creates a `ListContext`. In addition, pass a form component to be displayed when the user switches to edit or create mode. -{% raw %} ```tsx -import { - List, - TextField, - TextInput, - DateField, - DateInput, - SelectField, - SelectInput, - required, -} from 'react-admin'; +import { List, TextField, TextInput, DateField, DateInput, SelectField, SelectInput, required } from 'react-admin'; import { EditableDatagrid, RowForm } from '@react-admin/ra-editable-datagrid'; -const professionChoices = [ - { id: 'actor', name: 'Actor' }, - { id: 'singer', name: 'Singer' }, - { id: 'other', name: 'Other' }, -]; - -const ArtistForm = () => ( - <RowForm> - <TextField source="id" /> - <TextInput source="firstname" validate={required()} /> - <TextInput source="name" validate={required()} /> - <DateInput source="dob" label="born" validate={required()} /> - <SelectInput - source="prof" - label="Profession" - choices={professionChoices} - /> - </RowForm> -); - export const ArtistList = () => ( <List hasCreate empty={false}> <EditableDatagrid - mutationMode="undoable" createForm={<ArtistForm />} editForm={<ArtistForm />} > <TextField source="id" /> - <TextField source="firstname" /> - <TextField source="name" /> + <TextField source="firstName" /> + <TextField source="lastName" /> <DateField source="dob" label="born" /> - <SelectField - source="prof" - label="Profession" - choices={professionChoices} - /> + <SelectField source="profession" choices={professionChoices} /> </EditableDatagrid> </List> ); + +const ArtistForm = () => ( + <RowForm> + <TextField source="id" /> + <TextInput source="firstName" validate={required()} /> + <TextInput source="lastName" validate={required()} /> + <DateInput source="dob" label="born" validate={required()} /> + <SelectInput source="profession" choices={professions} /> + </RowForm> +); + +const professions = [ + { id: 'actor', name: 'Actor' }, + { id: 'singer', name: 'Singer' }, + { id: 'other', name: 'Other' }, +]; ``` -{% endraw %} +**Tip**: No need to include an `<EditButton>` in the datagrid children, as `<EditableDatagrid>` automatically adds a column with edit/delete buttons. ## Props +As `<EditableDatagrid>` is a drop-in replacement for `<Datagrid>`, it accepts all [the `<Datagrid>` props](./Datagrid.md#props), plus a few extra props: + | Prop | Required | Type | Default | Description | | -------------- | -------- | ------------ | ---------- | ------------------------------------------------------------------------- | | `editForm` | Required | ReactElement | - | The component to display instead of a row when the users edit a record. | @@ -100,39 +85,12 @@ export const ArtistList = () => ( | `mutationMode` | Optional | `string` | `undoable` | Mutation mode (`'undoable'`, `'pessimistic'` or `'optimistic'`). | | `noDelete` | Optional | boolean | - | Disable the inline Delete button. | -`<EditableDatagrid>` is a drop-in replacement for `<Datagrid>` it means that you can provide additional props to passed them down to [`<Datagrid>` component](./Datagrid.md#props). - -## `editForm` - -The component to display instead of a row when the users edit a record. It renders as many columns as the `<EditableDatagrid>` has children. - -The `<EditableDatagrid>` component renders the `editForm` elements in a `<table>`, so these elements should render a `<tr>`. We advise you to use the [`<RowForm>`](#rowform) component for `editForm`, which renders a `<tr>` by default. But you can also use your own component to render the row form ([see below](#rowform)). - -```tsx -const ArtistForm = () => ( - <RowForm> - <TextField source="id" /> - {/*...*/} - </RowForm> -); - -export const ArtistList = () => ( - <List> - <EditableDatagrid editForm={<ArtistForm />}> - <TextField source="id" /> - {/*...*/} - </EditableDatagrid> - </List> -); -``` - -**Tip**: No need to include an `<EditButton>` as child, the `<EditableDatagrid>` component adds a column with edit/delete/save/cancel buttons itself. - ## `actions` -By default, the `<EditableDatagrid>` will show both edit and delete buttons when users hover a row. If you want to either customize the buttons behavior or provide more actions, you can leverage the `actions` prop, which accepts a React element. For instance, here's how to customize the delete button so that it asks users for a confirmation but still allows to undo the deletion: +By default, the `<EditableDatagrid>` will show both edit and delete buttons when users hover a row. If you want to either customize the button's behavior or provide more actions, you can leverage the `actions` prop. It accepts a React element. + +For instance, here's how to customize the delete button so that it asks users for a confirmation but still allows them to undo the deletion: -{% raw %} ```tsx import React from 'react'; import { List, TextField } from 'react-admin'; @@ -143,96 +101,106 @@ import { } from '@react-admin/ra-editable-datagrid'; import { ArtistForm } from './ArtistForm'; -const CustomAction = () => ( - <> - <EditRowButton /> - <DeleteWithConfirmIconButton mutationMode="undoable" /> - </> -); - export const ArtistList = () => ( - <List hasCreate sort={{ field: 'id', order: 'DESC' }} empty={false}> + <List> <EditableDatagrid - actions={<CustomAction />} + actions={<RowAction />} // The mutation mode is still applied to updates mutationMode="undoable" editForm={<ArtistForm />} > <TextField source="id" /> - <TextField source="firstname" /> + <TextField source="firstName" /> <TextField source="name" /> </EditableDatagrid> </List> ); + +const RowAction = () => ( + <> + <EditRowButton /> + <DeleteWithConfirmIconButton mutationMode="undoable" /> + </> +); ``` -{% endraw %} ## `createForm` -The component to display as the first row when the user creates a record. - -The `<EditableDatagrid>` component renders the `createForm` elements in a `<table>`, so these elements should render a `<tr>`. We advise you to use the [`<RowForm>`](#rowform) component for `createForm`, which renders a `<tr>` by default. But you can also use your own component to render the row form ([see below](#rowform)) - -To display a create button on top of the list, you should add the [`hasCreate`](./List.md#hascreate) prop to the `<List>` component, as in the example below. +The component displayed as the first row when a user clicks on the Create button. It's usually a form built with [`<RowForm>`](#rowform), with the same number of children as the `<EditableDatagrid>` has children. ```tsx +export const ArtistList = () => ( + <List hasCreate empty={false}> + <EditableDatagrid + editForm={<ArtistForm />} + createForm={<ArtistForm />} + > + <TextField source="id" /> + <TextField source="firstName" /> + <TextField source="lastName" /> + {/*...*/} + </EditableDatagrid> + </List> +); + const ArtistForm = () => ( <RowForm> <TextField source="id" /> + <TextInput source="firstName" /> + <TextInput source="lastName" /> {/*...*/} </RowForm> ); +``` +**Tip**: It's a good idea to reuse the same form component for `createForm` and `editForm`, as in the example above. + +Since the creation form is embedded in the List view, you shouldn't set the `<Resource create>` prop. But react-admin's `<List>` only displays a Create button if the current `Resource` has a `create` page. That's why you must force the [`<List hasCreate>`](./List.md#hascreate) prop to `true`, as in the example above, to have the Create button show up with `<EditableDatagrid>`. + +Also, when the list is empty, the `<List>` component normally doesn't render its children (it renders an `empty` component instead). To bypass this system and see the empty editable datagrid with a create button instead, you need to force the `<List empty={false}>` prop, as in the example above. + +`<EditableDatagrid>` renders the `createForm` elements in a `<table>`, so the create form element should render a `<tr>`. We advise you to use the [`<RowForm>`](#rowform) component, which renders a `<tr>` by default. But you can also use your own component to render the creation form ([see `<RowForm>` below](#rowform)). + +**Tip**: The `createForm` component must render as many columns as there are children in the `<EditableDatagrid>`. That's why in the example above, the `<ArtistForm>` component renders a `<TextInput>` for each `<TextField>` (except for the read-only `id` field, for which it renders a `<TextField>`). + +## `editForm` + +The component displayed when a user clicks on a row to edit it. It's usually a form built with [`<RowForm>`](#rowform), with the same number of children as the `<EditableDatagrid>` has children. + +```tsx export const ArtistList = () => ( - <List hasCreate> - <EditableDatagrid createForm={<ArtistForm />}> + <List> + <EditableDatagrid editForm={<ArtistForm />}> <TextField source="id" /> + <TextField source="firstName" /> + <TextField source="lastName" /> {/*...*/} </EditableDatagrid> </List> ); -``` - -To be able to add a new row when the list is empty, you need to bypass the default `<List>` empty page system by passing `empty={false}` as `<List>` prop. -```tsx -export const ArtistList = () => ( - <List hasCreate empty={false}> - <EditableDatagrid createForm={<ArtistForm />}> - {/*...*/} - </EditableDatagrid> - </List> +const ArtistForm = () => ( + <RowForm> + <TextField source="id" /> + <TextInput source="firstName" /> + <TextInput source="lastName" /> + {/*...*/} + </RowForm> ); ``` -**Tip**: To display a custom create button, pass a custom component as the [`empty`](./Datagrid.md#empty) `Datagrid>` prop. It can use the [`useEditableDatagridContext`](#useeditabledatagridcontext) hook that allows you to manage the visibility of the creation form. +**Tip**: No need to include a `<SaveButton>` in the form, as `<RowForm>` automatically adds a column with save/cancel buttons. -```tsx -const MyCreateButton = () => ( - <Box> - <Typography>No books yet</Typography> - <Typography>Do you want to add one?</Typography> - <CreateButton label="Create the first book" /> - </Box> -); +**Tip**: If one column isn't editable, use a `<Field>` component instead of an `<Input>` component (like the `<TextField>` in the `<RowForm>` above). -export const ArtistList = () => ( - <List hasCreate empty={false}> - <EditableDatagrid createForm={<ArtistForm />} empty={<MyCreateButton />}> - {/*...*/} - </EditableDatagrid> - </List> -); -``` - -Feel free to visit the [dedicated stories](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-empty--custom-empty-in-list) to see more examples. +The `<EditableDatagrid>` component renders the `editForm` elements in a `<table>`, so these elements should render a `<tr>`. We advise you to use [the `<RowForm>` component](#rowform) for `editForm`, which renders a `<tr>` by default. But you can also use your own component to render the row form. ## `mutationMode` -Use the `mutationMode` prop to specify the [mutation mode](./Edit.html#mutationmode). By default, the `<EditableDatagrid>` uses the `undoable` mutation mode. +Use the `mutationMode` prop to specify the [mutation mode](./Edit.html#mutationmode) for the edit and delete actions. By default, the `<EditableDatagrid>` uses the `undoable` mutation mode. You can change it to `optimistic` or `pessimistic` if you prefer. ```jsx -<EditableDatagrid mutationMode="undoable"> +<EditableDatagrid mutationMode="pessimistic"> {/*...*/} </EditableDatagrid> ``` @@ -242,14 +210,44 @@ Use the `mutationMode` prop to specify the [mutation mode](./Edit.html#mutationm You can disable the delete button by setting the `noDelete` prop to `true`: ```jsx -<EditableDatagrid noDelete={true}> +<EditableDatagrid noDelete> {/*...*/} </EditableDatagrid> ``` ## `<RowForm>` -`<RowForm>` renders a table row with one cell per child. That means that `<RowForm>` and `<EditableDatagrid>` should have the same number of children, and these children should concern the same `source`. +`<RowForm>` renders a form in a table row, with one table cell per child. It is designed to be used as [`editForm`](#editform) and [`createForm`](#createform) element. + +```tsx +import { List, TextField, TextInput } from 'react-admin'; +import { EditableDatagrid, RowForm } from '@react-admin/ra-editable-datagrid'; + +export const ArtistList = () => ( + <List hasCreate empty={false}> + <EditableDatagrid + createForm={<ArtistForm />} + editForm={<ArtistForm />} + > + <TextField source="id" /> + <TextField source="firstName" /> + <TextField source="lastName" /> + </EditableDatagrid> + </List> +); + +const ArtistForm = () => ( + <RowForm> + <TextField source="id" /> + <TextInput source="firstName" /> + <TextInput source="lastName" /> + </RowForm> +); +``` + +`<RowForm>` and `<EditableDatagrid>` should have the same number of children, and these children should concern the same `source`. + +**Tip**: No need to include a `<SaveButton>` in the form, as `<RowForm>` automatically adds a column with save/cancel buttons. If you want to avoid the edition of a column, use a `<Field>` component instead of an `<Input>` component (like the `<TextField>` in the example above). @@ -261,14 +259,14 @@ If you want to avoid the edition of a column, use a `<Field>` component instead | `submitOnEnter` | Optional | `function` | - | A function to transform the row before it is saved. | | `transform` | Optional | `boolean` | `true` | Whether the form can be submitted by pressing the Enter key. | -Any additional props passed to `<RowForm>` are passed to the underlying react-admin [`<Form>`](./Form.md) component. That means that you can pass e.g. `defaultValues`, or `validate` props. +Any additional props passed to `<RowForm>` are passed down to the underlying react-admin [`<Form>`](./Form.md) component. That means that you can pass e.g. `defaultValues`, or `validate` props. {% raw %} ```tsx import { RowForm } from '@react-admin/ra-editable-datagrid'; const ArtistForm = () => ( - <RowForm defaultValues={{ firstname: 'John', name: 'Doe' }}> + <RowForm defaultValues={{ firstName: 'John', name: 'Doe' }}> <TextField source="id" disabled /> <TextInput source="name" validate={required()} /> </RowForm> @@ -283,6 +281,8 @@ For advanced use cases, you can use the `useEditableDatagridContext` hook to man - `openStandaloneCreateForm`: A function to open the create form. - `closeStandaloneCreateForm`: A function to close the create form. +For instance, the following example displays a custom message when the list is empty, and a button to open the create form: + ```tsx import React from 'react'; import { Typography, Box } from '@mui/material'; @@ -317,15 +317,12 @@ export const BookList = () => ( Feel free to visit the [dedicated stories](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-empty--custom-empty-standalone) to see more examples. -## Recipes +## Using Inside a `<ReferenceManyField>` -### Inside A `<ReferenceManyField>` - -Here is another example inside a `<ReferenceManyField>`. The only difference with its usage in a `<List>` is that you have to initialize the foreign key in the create form using the `defaultValues` prop: +You can use `<EditableDatagrid>` inside a `<ReferenceManyField>`. The only difference with its usage in a `<List>` is that you have to initialize the foreign key in the creation form using the `defaultValues` prop: {% raw %} ```tsx -import React from 'react'; import { DateField, DateInput, @@ -358,7 +355,7 @@ const OrderEdit = () => ( > <TextField source="id" /> <TextField source="name" /> - <NumberField source="price" label="Default Price" /> + <NumberField source="price" /> <DateField source="available_since" /> </EditableDatagrid> </ReferenceManyField> @@ -373,11 +370,7 @@ const ProductForm = () => { <RowForm defaultValues={{ order_id: getValues('id') }}> <TextInput source="id" disabled /> <TextInput source="name" validate={required()} /> - <NumberInput - source="price" - label="Default Price" - validate={required()} - /> + <NumberInput source="price" validate={required()} /> <DateInput source="available_since" validate={required()} /> </RowForm> ); @@ -385,18 +378,19 @@ const ProductForm = () => { ``` {% endraw %} -In these examples, the same form component is used in `createForm` and `editForm`, but you can pass different forms (e.g. if some fields can be set at creation but not changed afterwards). +In these examples, the same form component is used in `createForm` and `editForm`, but you can pass different forms (e.g. if some fields can be set at creation but not changed afterward). -### Providing Custom Side Effects +**Tip**: To edit a one-to-many relationship, you can also use [the `<ReferenceManyInput>` component](./ReferenceManyInput.md). -Like other forms in react-admin, you can provide your own side effects in response to successful or failed actions by passing functions to the `onSuccess` or `onError` inside the `mutationOptions` prop: +## Providing Custom Side Effects + +You can provide your own side effects in response to successful or failed save and delete actions, by passing functions to the `onSuccess` or `onError` inside the `mutationOptions` prop: {% raw %} ```tsx -import { TextInput, DateInput, SelectInput, useNotify } from 'react-admin'; import { RowForm, useRowContext } from '@react-admin/ra-editable-datagrid'; -const ArtistEditForm = () => { +const ArtistEditionForm = () => { const notify = useNotify(); const { close } = useRowContext(); @@ -409,20 +403,12 @@ const ArtistEditForm = () => { return ( <RowForm mutationOptions={{ onSuccess: handleSuccess }}> - <TextField source="id" /> - <TextInput source="firstname" validate={required()} /> - <TextInput source="name" validate={required()} /> - <DateInput source="dob" label="born" validate={required()} /> - <SelectInput - source="prof" - label="Profession" - choices={professionChoices} - /> + {/*...*/} </RowForm> ); }; -const ArtistCreateForm = () => { +const ArtistCreationForm = () => { const notify = useNotify(); const { close } = useRowContext(); @@ -433,21 +419,14 @@ const ArtistCreateForm = () => { return ( <RowForm mutationOptions={{ onSuccess: handleSuccess }}> - <TextInput source="firstname" validate={required()} /> - <TextInput source="name" validate={required()} /> - <DateInput source="dob" label="born" validate={required()} /> - <SelectInput - source="prof" - label="Profession" - choices={professionChoices} - /> + {/*...*/} </RowForm> ); }; ``` {% endraw %} -Note that we provide an additional side effects hook: `useRowContext` which allows you to close the form. +Note that we provide an additional side effects hook: `useRowContext` allows you to close the form. **Tip**: If you use `useNotify` inside an `onSuccess` side effect for an Edit form in addition to the `<EditableDatagrid mutationMode="undoable">` prop, you will need to set the notification as undoable for the changes to take effects. Also, note that, on undoable forms, the `onSuccess` side effect will be called immediately, without any `response` argument. @@ -458,7 +437,7 @@ const handleSuccess = () => { }; ``` -Besides, the `<RowForm>` also accept a function for its `transform` prop allowing you to alter the data before sending it to the dataProvider: +Besides, the `<RowForm>` also accepts a function for its `transform` prop allowing you to alter the data before sending it to the dataProvider: ```tsx import { TextInput, DateInput, SelectInput } from 'react-admin'; @@ -474,7 +453,7 @@ const ArtistCreateForm = () => { return ( <RowForm transform={handleTransform}> - <TextInput source="firstname" validate={required()} /> + <TextInput source="firstName" validate={required()} /> <TextInput source="name" validate={required()} /> <DateInput source="dob" label="born" validate={required()} /> <SelectInput @@ -487,7 +466,7 @@ const ArtistCreateForm = () => { }; ``` -### Adding A `meta` Prop To All Mutations +## Adding A `meta` Prop To All Mutations Just like with `<Datagrid>`, if you'd like to add a `meta` prop to all the dataProvider calls, you will need to provide custom `mutationOptions` at all the places where mutations occur: @@ -499,7 +478,6 @@ Here is a complete example: {% raw %} ```tsx -import * as React from 'react'; import { TextInput, DateInput, @@ -520,11 +498,11 @@ import { const ArtistForm = ({ meta }) => ( <RowForm - defaultValues={{ firstname: 'John', name: 'Doe' }} + defaultValues={{ firstName: 'John', name: 'Doe' }} mutationOptions={{ meta }} > <TextField source="id" /> - <TextInput source="firstname" validate={required()} /> + <TextInput source="firstName" validate={required()} /> <TextInput source="name" validate={required()} /> <DateInput source="dob" label="Born" validate={required()} /> <SelectInput @@ -551,7 +529,7 @@ const ArtistListWithMeta = () => { } > <TextField source="id" /> - <TextField source="firstname" /> + <TextField source="firstName" /> <TextField source="name" /> <DateField source="dob" label="Born" /> <SelectField @@ -566,7 +544,7 @@ const ArtistListWithMeta = () => { ``` {% endraw %} -### Configurable +## Configurable You can let end users customize what fields are displayed in the `<EditableDatagrid>` by using the `<EditableDatagridConfigurable>` component instead, together with the `<RowFormConfigurable>` component. @@ -589,7 +567,7 @@ const ArtistForm = ({ meta }) => ( - <RowForm> + <RowFormConfigurable> <TextField source="id" /> - <TextInput source="firstname" validate={required()} /> + <TextInput source="firstName" validate={required()} /> <TextInput source="name" validate={required()} /> <DateInput source="dob" label="Born" validate={required()} /> <SelectInput @@ -610,7 +588,7 @@ const ArtistList = () => ( editForm={<ArtistForm />} > <TextField source="id" /> - <TextField source="firstname" /> + <TextField source="firstName" /> <TextField source="name" /> <DateField source="dob" label="born" /> <SelectField @@ -624,7 +602,7 @@ const ArtistList = () => ( ); ``` -When users enter the configuration mode and select the `<EditableDatagrid>`, they can show / hide datagrid columns. They can also use the [`<SelectColumnsButton>`](./SelectColumnsButton.md) +When users enter the configuration mode and select the `<EditableDatagrid>`, they can show/hide datagrid columns. They can also use the [`<SelectColumnsButton>`](./SelectColumnsButton.md) By default, `<EditableDatagridConfigurable>` renders all child fields. But you can also omit some of them by passing an `omit` prop containing an array of field sources: @@ -643,7 +621,7 @@ const PostList = () => ( ); ``` -If you render more than one `<EditableDatagridConfigurable>` in the same page, you must pass a unique `preferenceKey` prop to each one: +If you render more than one `<EditableDatagridConfigurable>` on the same page, you must pass a unique `preferenceKey` prop to each one: ```tsx const PostList = () => ( @@ -677,4 +655,4 @@ const PostList = () => ( ); ``` -`<EditableDatagridConfigurable>` accepts the same props as `<EditableDatagrid>`. \ No newline at end of file +`<EditableDatagridConfigurable>` accepts the same props as `<EditableDatagrid>`. From 415edfc54f5b4b74039036e42b7133f2ade95859 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Wed, 6 Dec 2023 18:16:29 +0100 Subject: [PATCH 57/72] [Doc] Update `<DatagridAG>` doc to document lazy loading --- docs/DatagridAG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/DatagridAG.md b/docs/DatagridAG.md index 080ccab0f2f..1a45e3d9552 100644 --- a/docs/DatagridAG.md +++ b/docs/DatagridAG.md @@ -88,6 +88,8 @@ Here are the important things to note: `<DatagridAG>` doesn't currently support the [server-side row model](https://www.ag-grid.com/react-data-grid/row-models/), so you have to load all data client-side. The client-side performance isn't affected by a large number of records, as ag-grid uses [DOM virtualization](https://www.ag-grid.com/react-data-grid/dom-virtualisation/). `<DatagridAG>` has been tested with 10,000 records without any performance issue. +> **Note:** To mitigate an [issue](https://github.com/ag-grid/ag-grid/issues/7241) preventing tree shaking with some bundlers, `<DatagridAG>` uses React's [lazy loading](https://react.dev/reference/react/lazy#suspense-for-code-splitting) by default. This feature relies on [dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import), which might require support from your bundler or framework. + ## Props | Prop | Required | Type | Default | Description | From 6503cbdc96861d3bcf71b81415239a944603301e Mon Sep 17 00:00:00 2001 From: Carlos Izquierdo <gheesh@gmail.com> Date: Wed, 6 Dec 2023 21:52:33 +0100 Subject: [PATCH 58/72] Removed postgrest from auth providers, see #105 of ra-data-postgrest https://github.com/raphiniert-com/ra-data-postgrest/issues/105 --- docs/AuthProviderList.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/AuthProviderList.md b/docs/AuthProviderList.md index 391476e9a03..4f496296a05 100644 --- a/docs/AuthProviderList.md +++ b/docs/AuthProviderList.md @@ -16,7 +16,6 @@ It's very common that your auth logic is so specific that you'll need to write y - **[Firebase Auth (Google, Facebook, GitHub, etc.)](https://firebase.google.com/docs/auth/web/firebaseui)**: [benwinding/react-admin-firebase](https://github.com/benwinding/react-admin-firebase#auth-provider) - **[Google Identity & Google Workspace](https://developers.google.com/identity/gsi/web/guides/overview)**: [marmelab/ra-auth-google](https://github.com/marmelab/ra-auth-google/blob/main/packages/ra-auth-google/Readme.md) - **[Keycloak](https://www.keycloak.org/)**: [marmelab/ra-keycloak](https://github.com/marmelab/ra-keycloak/blob/main/packages/ra-keycloak/Readme.md) -- **[Postgrest](https://postgrest.org/)**: [raphiniert-com/ra-data-postgrest](https://github.com/raphiniert-com/ra-data-postgrest) - **[Supabase](https://supabase.io/)**: [marmelab/ra-supabase](https://github.com/marmelab/ra-supabase/blob/main/packages/ra-supabase/README.md) - **[SurrealDB](https://surrealdb.com/)**: [djedi23/ra-surrealdb](https://github.com/djedi23/ra-surrealdb) From 11d3fe3287c0e6b4eca4f0256abdc1fab8531802 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Thu, 7 Dec 2023 11:23:27 +0100 Subject: [PATCH 59/72] [no ci] Apply suggestions from code review --- docs/EditableDatagrid.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/EditableDatagrid.md b/docs/EditableDatagrid.md index a6dc52e188b..c993ff6b1c4 100644 --- a/docs/EditableDatagrid.md +++ b/docs/EditableDatagrid.md @@ -19,7 +19,7 @@ With `<EditableDatagrid>`, when users click on a row in the datagrid, the row co You can test it live in [the Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-editable-datagrid-editabledatagrid--undoable) and [the e-commerce demo](https://marmelab.com/ra-enterprise-demo/#/tours/ra-editable-datagrid). -`<EditableDatagrid>` allows you to use any [Input component](./ImageInput.md) to edit the record - including Reference Inputs for foreign keys. +`<EditableDatagrid>` allows you to use any [Input component](./Inputs.md) to edit the record - including Reference Inputs for foreign keys. ## Usage @@ -256,8 +256,8 @@ If you want to avoid the edition of a column, use a `<Field>` component instead | Prop | Required | Type | Default | Description | | ----------------- | -------- | ------------ | ------- | ------------------------------------------------------------------------- | | `mutationOptions` | Optional | `Object` | - | An object that can contain `onSuccess` and `onError` functions to be executed after the row has been saved and after the row has failed to be saved respectively. | -| `submitOnEnter` | Optional | `function` | - | A function to transform the row before it is saved. | -| `transform` | Optional | `boolean` | `true` | Whether the form can be submitted by pressing the Enter key. | +| `submitOnEnter` | Optional | `boolean` | `true` | Whether the form can be submitted by pressing the Enter key. | +| `transform` | Optional | `function` | - | A function to transform the row before it is saved. | Any additional props passed to `<RowForm>` are passed down to the underlying react-admin [`<Form>`](./Form.md) component. That means that you can pass e.g. `defaultValues`, or `validate` props. From 7dad47883111a8d0ecf2235dd020d2b0fc800af1 Mon Sep 17 00:00:00 2001 From: adrien guernier <adrien@marmelab.com> Date: Thu, 7 Dec 2023 11:53:22 +0100 Subject: [PATCH 60/72] doc: fix TS warning in Connecting To A Real API tutorial --- docs/Tutorial.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 203050484ab..391da233af6 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -1059,7 +1059,7 @@ export const dataProvider: DataProvider = { return httpClient(url).then(({ headers, json }) => ({ data: json, - total: parseInt((headers.get('content-range') || "0").split('/').pop() || 0, 10), + total: parseInt((headers.get('content-range') || "0").split('/').pop() || '0', 10), })); }, @@ -1091,7 +1091,7 @@ export const dataProvider: DataProvider = { return httpClient(url).then(({ headers, json }) => ({ data: json, - total: parseInt((headers.get('content-range') || "0").split('/').pop() || 0, 10), + total: parseInt((headers.get('content-range') || "0").split('/').pop() || '0', 10), })); }, @@ -1116,7 +1116,7 @@ export const dataProvider: DataProvider = { method: 'POST', body: JSON.stringify(params.data), }).then(({ json }) => ({ - data: { ...params.data, id: json.id }, + data: { ...params.data, id: json.id } as any, })), delete: (resource, params) => From 3a6aaf8d7896a68baeb7beab609bc913006a62bc Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:04:08 +0100 Subject: [PATCH 61/72] Fix useTheme may return undefined --- .../src/theme/useTheme.spec.tsx | 42 ++++++++++++++++++- .../ra-ui-materialui/src/theme/useTheme.ts | 9 +++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index b0923706afa..39bf1b0b700 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -4,6 +4,7 @@ import expect from 'expect'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { useTheme } from './useTheme'; +import { act } from 'react-test-renderer'; const authProvider = { login: jest.fn().mockResolvedValueOnce(''), @@ -23,13 +24,49 @@ const Foo = () => { }; describe('useTheme', () => { - it('should return undefined by default', () => { + beforeEach(() => { + window.matchMedia = jest.fn(query => ({ + matches: false, + media: query, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + onchange: jest.fn(), + })); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return the light theme by default', () => { + render( + <CoreAdminContext authProvider={authProvider}> + <Foo /> + </CoreAdminContext> + ); + expect(screen.queryByText('light')).not.toBeNull(); + }); + + it('should return the user preferred theme by default', () => { + window.matchMedia = jest.fn(query => ({ + matches: true, + media: query, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + onchange: jest.fn(), + })); render( <CoreAdminContext authProvider={authProvider}> <Foo /> </CoreAdminContext> ); - expect(screen.queryByLabelText('has-theme')).toBeNull(); + expect(screen.queryByText('dark')).not.toBeNull(); }); it('should return current theme when set', () => { @@ -42,6 +79,7 @@ describe('useTheme', () => { </CoreAdminContext> ); expect(screen.getByLabelText('has-theme')).not.toBeNull(); + expect(screen.queryByText('dark')).not.toBeNull(); }); it('should return theme from settings when available', () => { diff --git a/packages/ra-ui-materialui/src/theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts index ec3ead4cdd7..4445d2e2081 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.ts +++ b/packages/ra-ui-materialui/src/theme/useTheme.ts @@ -1,5 +1,6 @@ import { useStore } from 'ra-core'; import { RaThemeOptions, ThemeType } from './types'; +import { useMediaQuery } from '@mui/material'; export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; @@ -23,7 +24,13 @@ export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; export const useTheme = ( type?: ThemeType | RaThemeOptions ): [ThemeType | RaThemeOptions, ThemeSetter] => { + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { + noSsr: true, + }); // FIXME: remove legacy mode in v5, and remove the RaThemeOptions type - const [theme, setter] = useStore<ThemeType | RaThemeOptions>('theme', type); + const [theme, setter] = useStore<ThemeType | RaThemeOptions>( + 'theme', + type ?? prefersDarkMode ? 'dark' : 'light' + ); return [theme, setter]; }; From 9f8739ba5be0aa7ff0134ee77b217c1cbc02e761 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:46:11 +0100 Subject: [PATCH 62/72] Fix tests --- .../src/theme/useTheme.spec.tsx | 37 +++---------------- .../ra-ui-materialui/src/theme/useTheme.ts | 2 +- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index 39bf1b0b700..bf4765dbf6c 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -4,7 +4,7 @@ import expect from 'expect'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { useTheme } from './useTheme'; -import { act } from 'react-test-renderer'; +import { ThemeTestWrapper } from '../layout/ThemeTestWrapper'; const authProvider = { login: jest.fn().mockResolvedValueOnce(''), @@ -24,23 +24,6 @@ const Foo = () => { }; describe('useTheme', () => { - beforeEach(() => { - window.matchMedia = jest.fn(query => ({ - matches: false, - media: query, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - onchange: jest.fn(), - })); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - it('should return the light theme by default', () => { render( <CoreAdminContext authProvider={authProvider}> @@ -51,20 +34,12 @@ describe('useTheme', () => { }); it('should return the user preferred theme by default', () => { - window.matchMedia = jest.fn(query => ({ - matches: true, - media: query, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - onchange: jest.fn(), - })); render( - <CoreAdminContext authProvider={authProvider}> - <Foo /> - </CoreAdminContext> + <ThemeTestWrapper mode="dark"> + <CoreAdminContext authProvider={authProvider}> + <Foo /> + </CoreAdminContext> + </ThemeTestWrapper> ); expect(screen.queryByText('dark')).not.toBeNull(); }); diff --git a/packages/ra-ui-materialui/src/theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts index 4445d2e2081..f46ac11b541 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.ts +++ b/packages/ra-ui-materialui/src/theme/useTheme.ts @@ -30,7 +30,7 @@ export const useTheme = ( // FIXME: remove legacy mode in v5, and remove the RaThemeOptions type const [theme, setter] = useStore<ThemeType | RaThemeOptions>( 'theme', - type ?? prefersDarkMode ? 'dark' : 'light' + type ?? (prefersDarkMode ? 'dark' : 'light') ); return [theme, setter]; }; From d5b1fe6a0bfa431a69c18a9c6ff844dec458d6e7 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:19:43 +0100 Subject: [PATCH 63/72] Ensure light theme is used when no dark theme is provided --- .../src/theme/useTheme.spec.tsx | 34 +++++++++++++++++-- .../ra-ui-materialui/src/theme/useTheme.ts | 4 ++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index bf4765dbf6c..25a27fe98ba 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -4,7 +4,9 @@ import expect from 'expect'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { useTheme } from './useTheme'; +import { AdminContext } from '../AdminContext'; import { ThemeTestWrapper } from '../layout/ThemeTestWrapper'; +import { defaultDarkTheme } from './defaultTheme'; const authProvider = { login: jest.fn().mockResolvedValueOnce(''), @@ -33,7 +35,7 @@ describe('useTheme', () => { expect(screen.queryByText('light')).not.toBeNull(); }); - it('should return the user preferred theme by default', () => { + it('should return the light theme when no dark theme is provided even though user prefers dark mode', () => { render( <ThemeTestWrapper mode="dark"> <CoreAdminContext authProvider={authProvider}> @@ -41,7 +43,35 @@ describe('useTheme', () => { </CoreAdminContext> </ThemeTestWrapper> ); - expect(screen.queryByText('dark')).not.toBeNull(); + expect(screen.queryByText('light')).not.toBeNull(); + }); + + it('should return the user preferred theme by default', async () => { + const ssrMatchMedia = query => ({ + matches: query === '(prefers-color-scheme: dark)' ? true : false, + }); + + render( + <ThemeTestWrapper mode="dark"> + <AdminContext + authProvider={authProvider} + darkTheme={{ + ...defaultDarkTheme, + components: { + MuiUseMediaQuery: { + defaultProps: { + ssrMatchMedia, + matchMedia: ssrMatchMedia, + }, + }, + }, + }} + > + <Foo /> + </AdminContext> + </ThemeTestWrapper> + ); + await screen.findByText('dark'); }); it('should return current theme when set', () => { diff --git a/packages/ra-ui-materialui/src/theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts index f46ac11b541..1f460e46da9 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.ts +++ b/packages/ra-ui-materialui/src/theme/useTheme.ts @@ -1,6 +1,7 @@ import { useStore } from 'ra-core'; import { RaThemeOptions, ThemeType } from './types'; import { useMediaQuery } from '@mui/material'; +import { useThemesContext } from './useThemesContext'; export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; @@ -24,13 +25,14 @@ export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; export const useTheme = ( type?: ThemeType | RaThemeOptions ): [ThemeType | RaThemeOptions, ThemeSetter] => { + const { darkTheme } = useThemesContext(); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true, }); // FIXME: remove legacy mode in v5, and remove the RaThemeOptions type const [theme, setter] = useStore<ThemeType | RaThemeOptions>( 'theme', - type ?? (prefersDarkMode ? 'dark' : 'light') + type ?? (prefersDarkMode && darkTheme ? 'dark' : 'light') ); return [theme, setter]; }; From 75c450e8499ccc579c8ca1614d3f3f6c10eec544 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 8 Dec 2023 18:06:09 +0100 Subject: [PATCH 64/72] Ensure light theme is used when no dark theme is provided and store has dark theme --- .../src/theme/useTheme.spec.tsx | 37 +++++++++++++++++-- .../ra-ui-materialui/src/theme/useTheme.ts | 4 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index 25a27fe98ba..886a215df31 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -46,6 +46,16 @@ describe('useTheme', () => { expect(screen.queryByText('light')).not.toBeNull(); }); + it('should return the light theme when no dark theme is provided even though the stored theme is dark', () => { + const store = memoryStore({ theme: 'dark' }); + render( + <CoreAdminContext authProvider={authProvider} store={store}> + <Foo /> + </CoreAdminContext> + ); + expect(screen.queryByText('light')).not.toBeNull(); + }); + it('should return the user preferred theme by default', async () => { const ssrMatchMedia = query => ({ matches: query === '(prefers-color-scheme: dark)' ? true : false, @@ -76,23 +86,42 @@ describe('useTheme', () => { it('should return current theme when set', () => { render( - <CoreAdminContext + <AdminContext authProvider={authProvider} store={memoryStore({ theme: 'dark' })} + darkTheme={defaultDarkTheme} > <Foo /> - </CoreAdminContext> + </AdminContext> ); expect(screen.getByLabelText('has-theme')).not.toBeNull(); expect(screen.queryByText('dark')).not.toBeNull(); }); it('should return theme from settings when available', () => { - const { result: storeResult } = renderHook(() => useStore('theme')); + const { result: storeResult } = renderHook(() => useStore('theme'), { + wrapper: ({ children }) => ( + <AdminContext + authProvider={authProvider} + darkTheme={defaultDarkTheme} + > + {children} + </AdminContext> + ), + }); const [_, setTheme] = storeResult.current; setTheme('dark'); - const { result: themeResult } = renderHook(() => useTheme()); + const { result: themeResult } = renderHook(() => useTheme(), { + wrapper: ({ children }) => ( + <AdminContext + authProvider={authProvider} + darkTheme={defaultDarkTheme} + > + {children} + </AdminContext> + ), + }); const [theme, __] = themeResult.current; expect(theme).toEqual('dark'); diff --git a/packages/ra-ui-materialui/src/theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts index 1f460e46da9..72954324d49 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.ts +++ b/packages/ra-ui-materialui/src/theme/useTheme.ts @@ -34,5 +34,7 @@ export const useTheme = ( 'theme', type ?? (prefersDarkMode && darkTheme ? 'dark' : 'light') ); - return [theme, setter]; + + // Ensure that even though the store has its value set to 'dark', we still use the light theme when no dark theme is available + return [darkTheme != null ? theme : 'light', setter]; }; From e91ac34cc545a917354c20c42553e9f46c806a59 Mon Sep 17 00:00:00 2001 From: David Vergnault <david.vergnault@moank.com> Date: Mon, 11 Dec 2023 16:35:41 +0100 Subject: [PATCH 65/72] Scroll on top on datagrid row click --- packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx index 90e859c30bb..1cf65d4c9ac 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx @@ -115,7 +115,9 @@ const DatagridRow: FC<DatagridRowProps> = React.forwardRef((props, ref) => { return; } if (['edit', 'show'].includes(type)) { - navigate(createPath({ resource, id, type })); + navigate(createPath({ resource, id, type }), { + state: { _scrollToTop: true }, + }); return; } if (type === 'expand') { From 47fae2342c5543c8ca2bd9492c2a49aea786f10e Mon Sep 17 00:00:00 2001 From: David Vergnault <david.vergnault@moank.com> Date: Mon, 11 Dec 2023 16:35:52 +0100 Subject: [PATCH 66/72] Scroll on top on ReferenceField click --- packages/ra-ui-materialui/src/field/ReferenceField.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx index d6c010b6fc8..4c809370f8c 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx @@ -230,6 +230,7 @@ export const ReferenceFieldView = < to={link} className={ReferenceFieldClasses.link} onClick={stopPropagation} + state={{ _scrollToTop: true }} > {child} </Link> From 9dfabc5c6792ae2de71efa14e415160d1a08056d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:53:45 +0100 Subject: [PATCH 67/72] Fix `useInput` documentation regarding errors display --- docs/useInput.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/useInput.md b/docs/useInput.md index de524ef7a8a..0951a36975f 100644 --- a/docs/useInput.md +++ b/docs/useInput.md @@ -59,10 +59,10 @@ Additional props are passed to [react-hook-form's `useController` hook](https:// ```jsx // in LatLongInput.js import TextField from '@mui/material/TextField'; -import { useInput, required } from 'react-admin'; +import { useInput, required, InputHelperText } from 'react-admin'; const BoundedTextField = (props) => { - const { onChange, onBlur, label, ...rest } = props; + const { onChange, onBlur, label, helperText, ...rest } = props; const { field, fieldState: { isTouched, invalid, error }, @@ -81,12 +81,22 @@ const BoundedTextField = (props) => { {...field} label={label} error={(isTouched || isSubmitted) && invalid} - helperText={(isTouched || isSubmitted) && invalid ? error : ''} + helperText={helperText !== false || ((isTouched || isSubmitted) && invalid) + ? ( + <InputHelperText + touched={isTouched || isSubmitted} + error={error?.message} + helperText={helperText} + /> + ) + : '' + } required={isRequired} {...rest} /> ); }; + const LatLngInput = props => { const { source, ...rest } = props; @@ -99,6 +109,7 @@ const LatLngInput = props => { ); }; ``` + ## Usage with Material UI `<Select>` ```jsx From 74105b2887c74e45287717b2a8e51c6d59e57400 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:40:26 +0100 Subject: [PATCH 68/72] Fix `<ArrayInput>` does not work in `<FilterForm>` --- .../src/list/filter/FilterButton.stories.tsx | 117 +++++++++++++----- .../src/list/filter/FilterForm.spec.tsx | 25 +++- .../src/list/filter/FilterForm.tsx | 13 +- 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx index b2bf3fa622f..80126fdc8b4 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ListBase } from 'ra-core'; +import { ListBase, memoryStore } from 'ra-core'; import { Admin, Resource, @@ -14,7 +14,12 @@ import { SearchInput, } from 'react-admin'; import fakerestDataProvider from 'ra-data-fakerest'; -import { AutocompleteArrayInput } from '../../input'; +import { + ArrayInput, + AutocompleteArrayInput, + SimpleFormIterator, +} from '../../input'; +import { MemoryRouter } from 'react-router'; export default { title: 'ra-ui-materialui/list/filter/FilterButton', @@ -197,12 +202,43 @@ export const Basic = (args: { disableSaveQuery?: boolean }) => { <TextInput label="Nested" source="nested.foo" defaultValue="bar" />, ]; return ( - <Admin dataProvider={fakerestDataProvider(data)}> - <Resource - name="posts" - list={<PostList postFilters={postFilters} args={args} />} - /> - </Admin> + <MemoryRouter> + <Admin + dataProvider={fakerestDataProvider(data)} + store={memoryStore()} + > + <Resource + name="posts" + list={<PostList postFilters={postFilters} args={args} />} + /> + </Admin> + </MemoryRouter> + ); +}; + +export const WithArrayInput = (args: { disableSaveQuery?: boolean }) => { + const postFilters: React.ReactElement[] = [ + <ArrayInput source="title" label="Title include" alwaysOn> + <SimpleFormIterator disableReordering> + <TextInput source="" label="Title" helperText={false} /> + </SimpleFormIterator> + </ArrayInput>, + ]; + return ( + <MemoryRouter> + <Admin + dataProvider={fakerestDataProvider( + data, + process.env.NODE_ENV !== 'test' + )} + store={memoryStore()} + > + <Resource + name="posts" + list={<PostList postFilters={postFilters} args={args} />} + /> + </Admin> + </MemoryRouter> ); }; @@ -211,12 +247,17 @@ export const DisabledFilters = (args: { disableSaveQuery?: boolean }) => { <TextInput label="Title" source="title" disabled={true} />, ]; return ( - <Admin dataProvider={fakerestDataProvider(data)}> - <Resource - name="posts" - list={<PostList postFilters={postFilters} args={args} />} - /> - </Admin> + <MemoryRouter> + <Admin + dataProvider={fakerestDataProvider(data)} + store={memoryStore()} + > + <Resource + name="posts" + list={<PostList postFilters={postFilters} args={args} />} + /> + </Admin> + </MemoryRouter> ); }; @@ -242,12 +283,17 @@ export const WithSearchInput = (args: { />, ]; return ( - <Admin dataProvider={fakerestDataProvider(data)}> - <Resource - name="posts" - list={<PostList postFilters={postFilters} args={args} />} - /> - </Admin> + <MemoryRouter> + <Admin + dataProvider={fakerestDataProvider(data)} + store={memoryStore()} + > + <Resource + name="posts" + list={<PostList postFilters={postFilters} args={args} />} + /> + </Admin> + </MemoryRouter> ); }; @@ -307,19 +353,22 @@ export const WithAutoCompleteArrayInput = (args: { />, ]; return ( - <Admin - dataProvider={withNestedFiltersSupportDataProvider()} - dashboard={Dashboard} - > - <Resource - name="posts" - list={ - <PostList - postFilters={postFilters} - args={{ disableSaveQuery: args.disableSaveQuery }} - /> - } - /> - </Admin> + <MemoryRouter> + <Admin + dataProvider={withNestedFiltersSupportDataProvider()} + dashboard={Dashboard} + store={memoryStore()} + > + <Resource + name="posts" + list={ + <PostList + postFilters={postFilters} + args={{ disableSaveQuery: args.disableSaveQuery }} + /> + } + /> + </Admin> + </MemoryRouter> ); }; diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx index 226f162ed73..acec88b7582 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx @@ -11,7 +11,11 @@ import * as React from 'react'; import { AdminContext } from '../../AdminContext'; import { ReferenceInput, SelectInput, TextInput } from '../../input'; import { Filter } from './Filter'; -import { Basic, WithAutoCompleteArrayInput } from './FilterButton.stories'; +import { + Basic, + WithAutoCompleteArrayInput, + WithArrayInput, +} from './FilterButton.stories'; import { FilterForm, getFilterFormValues, @@ -194,6 +198,25 @@ describe('<FilterForm />', () => { expect(screen.queryByLabelText('Nested')).toBeNull(); }); + it('should provide a FormGroupContext', async () => { + render(<WithArrayInput />); + + fireEvent.click(await screen.findByLabelText('Add')); + fireEvent.change((await screen.findAllByLabelText('Title'))[0], { + target: { value: 'Sint dignissimos in architecto aut' }, + }); + fireEvent.click(await screen.findByLabelText('Add')); + + await waitFor(() => { + expect(screen.getAllByText('Title')).toHaveLength(3); + }); + fireEvent.change((await screen.findAllByLabelText('Title'))[1], { + target: { value: 'Sed quo et et fugiat modi' }, + }); + + await screen.findByText('1-2 of 2'); + }); + describe('mergeInitialValuesWithDefaultValues', () => { it('should correctly merge initial values with the default values of the alwaysOn filters', () => { const initialValues = { diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx index 90880a86855..5634f114716 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx @@ -9,6 +9,7 @@ import { import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import { + FormGroupsProvider, LabelPrefixContextProvider, ListFilterContextValue, useListContext, @@ -84,11 +85,13 @@ export const FilterForm = (props: FilterFormProps) => { return ( <FormProvider {...form}> - <FilterFormBase - onSubmit={handleFormSubmit} - filters={filters} - {...rest} - /> + <FormGroupsProvider> + <FilterFormBase + onSubmit={handleFormSubmit} + filters={filters} + {...rest} + /> + </FormGroupsProvider> </FormProvider> ); }; From 8aa6adda841ecf5804c5abc0fe8bc56564c3841a Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:41:02 +0100 Subject: [PATCH 69/72] Fix `<TabbedShowLayout>` displays its fields as full width blocks --- packages/ra-ui-materialui/src/detail/Tab.tsx | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/Tab.tsx b/packages/ra-ui-materialui/src/detail/Tab.tsx index d24ece1ca1e..376a834ecfa 100644 --- a/packages/ra-ui-materialui/src/detail/Tab.tsx +++ b/packages/ra-ui-materialui/src/detail/Tab.tsx @@ -2,7 +2,12 @@ import * as React from 'react'; import { isValidElement, ReactElement, ReactNode } from 'react'; import PropTypes from 'prop-types'; import { Link, useLocation } from 'react-router-dom'; -import { Tab as MuiTab, TabProps as MuiTabProps, Stack } from '@mui/material'; +import { + Tab as MuiTab, + TabProps as MuiTabProps, + Stack, + styled, +} from '@mui/material'; import { ResponsiveStyleValue } from '@mui/system'; import { useTranslate, RaRecord } from 'ra-core'; import clsx from 'clsx'; @@ -101,7 +106,7 @@ export const Tab = ({ }; const renderContent = () => ( - <Stack className={contentClassName} spacing={spacing} divider={divider}> + <Root className={contentClassName} spacing={spacing} divider={divider}> {React.Children.map(children, field => field && isValidElement<any>(field) ? ( <Labeled @@ -110,6 +115,7 @@ export const Tab = ({ 'ra-field', field.props.source && `ra-field-${field.props.source}`, + TabClasses.row, field.props.className )} > @@ -117,12 +123,29 @@ export const Tab = ({ </Labeled> ) : null )} - </Stack> + </Root> ); return context === 'header' ? renderHeader() : renderContent(); }; +const PREFIX = 'RaTab'; + +export const TabClasses = { + row: `${PREFIX}-row`, +}; + +const Root = styled(Stack, { + name: PREFIX, + overridesResolver: (props, styles) => styles.root, +})(({ theme }) => ({ + flex: 1, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + [`& .${TabClasses.row}`]: { + display: 'inline', + }, +})); + Tab.propTypes = { children: PropTypes.node, className: PropTypes.string, From 980fbf8eca4204181a1707ce9b5a7d87362ece71 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:59:42 +0100 Subject: [PATCH 70/72] Remove unnecessary styles --- packages/ra-ui-materialui/src/detail/Tab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/Tab.tsx b/packages/ra-ui-materialui/src/detail/Tab.tsx index 376a834ecfa..2a50a5202c6 100644 --- a/packages/ra-ui-materialui/src/detail/Tab.tsx +++ b/packages/ra-ui-materialui/src/detail/Tab.tsx @@ -138,9 +138,7 @@ export const TabClasses = { const Root = styled(Stack, { name: PREFIX, overridesResolver: (props, styles) => styles.root, -})(({ theme }) => ({ - flex: 1, - padding: `${theme.spacing(1)} ${theme.spacing(2)}`, +})(() => ({ [`& .${TabClasses.row}`]: { display: 'inline', }, From f2c4f18a841bfad81a4f1d55bab95f387108ab95 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Thu, 14 Dec 2023 11:52:13 +0100 Subject: [PATCH 71/72] fix tests --- .../src/list/filter/FilterButton.spec.tsx | 9 ++++++--- packages/ra-ui-materialui/src/theme/useTheme.spec.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx index e8121088de4..04b4878e11d 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx @@ -199,9 +199,12 @@ describe('<FilterButton />', () => { fireEvent.click(await screen.findByText('Sint...')); await screen.findByLabelText('Add filter'); - await waitFor(() => { - expect(screen.getAllByRole('checkbox')).toHaveLength(2); - }); + await waitFor( + () => { + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + }, + { timeout: 10000 } + ); fireEvent.click(screen.getByLabelText('Add filter')); fireEvent.click(await screen.findByText('Remove all filters')); diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index 886a215df31..115223da36d 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -19,7 +19,7 @@ const authProvider = { const Foo = () => { const [theme] = useTheme(); return theme !== undefined ? ( - <div aria-label="has-theme">{theme}</div> + <div aria-label="has-theme">{theme as any}</div> ) : ( <></> ); @@ -59,6 +59,8 @@ describe('useTheme', () => { it('should return the user preferred theme by default', async () => { const ssrMatchMedia = query => ({ matches: query === '(prefers-color-scheme: dark)' ? true : false, + addListener: () => {}, + removeListener: () => {}, }); render( @@ -100,7 +102,7 @@ describe('useTheme', () => { it('should return theme from settings when available', () => { const { result: storeResult } = renderHook(() => useStore('theme'), { - wrapper: ({ children }) => ( + wrapper: ({ children }: any) => ( <AdminContext authProvider={authProvider} darkTheme={defaultDarkTheme} @@ -113,7 +115,7 @@ describe('useTheme', () => { setTheme('dark'); const { result: themeResult } = renderHook(() => useTheme(), { - wrapper: ({ children }) => ( + wrapper: ({ children }: any) => ( <AdminContext authProvider={authProvider} darkTheme={defaultDarkTheme} From 04456ccb15910c5012103b94bebd5174266cf3fb Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser <jb@marmelab.com> Date: Thu, 14 Dec 2023 14:22:20 +0100 Subject: [PATCH 72/72] [no ci] Apply suggestions from code review Co-authored-by: Francois Zaninotto <francois@marmelab.com> --- docs/Features.md | 4 ++-- docs/InfiniteList.md | 8 ++++---- docs/List.md | 14 +++++++------- docs/useListController.md | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/Features.md b/docs/Features.md index 043cc95c17c..6f764ecdaa5 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -295,7 +295,7 @@ import { import { Link } from 'react-router-dom'; const PostList = () => { - const { data, page, total, setPage, isLoading } = useListController({ + const { data, page, total, setPage, isPending } = useListController({ sort: { field: 'published_at', order: 'DESC' }, perPage: 10, }); @@ -309,7 +309,7 @@ const PostList = () => { <Button icon={<PlusOutlined />}>Create</Button> </Link> </div> - <Card bodyStyle={{ padding: '0' }} loading={isLoading}> + <Card bodyStyle={{ padding: '0' }} loading={isPending}> <Table size="small" dataSource={data} diff --git a/docs/InfiniteList.md b/docs/InfiniteList.md index 4ab366fd77e..e6518aa5e18 100644 --- a/docs/InfiniteList.md +++ b/docs/InfiniteList.md @@ -220,8 +220,8 @@ const ProductList = () => ( <InfiniteListBase> <Container> <Typography variant="h4">All products</Typography> - <WithListContext render={({ isLoading, data }) => ( - !isLoading && ( + <WithListContext render={({ isPending, data }) => ( + !isPending && ( <Stack spacing={1}> {data.map(product => ( <Card key={product.id}> @@ -248,11 +248,11 @@ import { useInfiniteListController } from 'react-admin'; import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; const ProductList = () => { - const { isLoading, data } = useInfiniteListController(); + const { isPending, data } = useInfiniteListController(); return ( <Container> <Typography variant="h4">All products</Typography> - {!isLoading && ( + {!isPending && ( <Stack spacing={1}> {data.map(product => ( <Card key={product.id}> diff --git a/docs/List.md b/docs/List.md index 4744c043f92..917d730789f 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1184,8 +1184,8 @@ const ProductList = () => ( <ListBase> <Container> <Typography variant="h4">All products</Typography> - <WithListContext render={({ isLoading, data }) => ( - !isLoading && ( + <WithListContext render={({ isPending, data }) => ( + !isPending && ( <Stack spacing={1}> {data.map(product => ( <Card key={product.id}> @@ -1197,8 +1197,8 @@ const ProductList = () => ( </Stack> ) )} /> - <WithListContext render={({ isLoading, total }) => ( - !isLoading && <Typography>{total} results</Typography> + <WithListContext render={({ isPending, total }) => ( + !isPending && <Typography>{total} results</Typography> )} /> </Container> </ListBase> @@ -1214,11 +1214,11 @@ import { useListController } from 'react-admin'; import { Card, CardContent, Container, Stack, Typography } from '@mui/material'; const ProductList = () => { - const { isLoading, data, total } = useListController(); + const { isPending, data, total } = useListController(); return ( <Container> <Typography variant="h4">All products</Typography> - {!isLoading && ( + {!isPending && ( <Stack spacing={1}> {data.map(product => ( <Card key={product.id}> @@ -1229,7 +1229,7 @@ const ProductList = () => { ))} </Stack> )} - {!isLoading && <Typography>{total} results</Typography>} + {!isPending && <Typography>{total} results</Typography>} </Container> ); }; diff --git a/docs/useListController.md b/docs/useListController.md index f3eee4896d6..95fec8274f1 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -31,7 +31,7 @@ import { import { Link } from 'react-router-dom'; const PostList = () => { - const { data, page, total, setPage, isLoading } = useListController({ + const { data, page, total, setPage, isPending } = useListController({ sort: { field: 'published_at', order: 'DESC' }, perPage: 10, }); @@ -45,7 +45,7 @@ const PostList = () => { <Button icon={<PlusOutlined />}>Create</Button> </Link> </div> - <Card bodyStyle={{ padding: '0' }} loading={isLoading}> + <Card bodyStyle={{ padding: '0' }} loading={isPending}> <Table size="small" dataSource={data}