From b6880a86931d2b8f9731f30915be532c134cb974 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 3 Nov 2022 18:13:57 +0100 Subject: [PATCH 1/6] [Doc] Update the tutorial to use Vite instead of create-react-app --- docs/Tutorial.md | 703 +++++++++--------- examples/tutorial/index.html | 13 + examples/tutorial/package.json | 45 +- examples/tutorial/public/index.html | 40 - examples/tutorial/public/manifest.json | 15 - examples/tutorial/src/App.css | 41 + examples/tutorial/src/{App.js => App.tsx} | 29 +- examples/tutorial/src/Dashboard.js | 11 - examples/tutorial/src/Dashboard.tsx | 8 + examples/tutorial/src/MyUrlField.tsx | 15 + examples/tutorial/src/assets/react.svg | 1 + .../src/{authProvider.js => authProvider.ts} | 6 +- examples/tutorial/src/index.js | 8 - examples/tutorial/src/index.module.css | 5 - examples/tutorial/src/logo.svg | 7 - examples/tutorial/src/main.tsx | 9 + examples/tutorial/src/posts.js | 80 -- examples/tutorial/src/posts.tsx | 55 ++ .../tutorial/src/registerServiceWorker.js | 120 --- examples/tutorial/src/{users.js => users.tsx} | 17 +- examples/tutorial/src/vite-env.d.ts | 1 + examples/tutorial/tsconfig.json | 21 + examples/tutorial/tsconfig.node.json | 9 + examples/tutorial/vite.config.ts | 7 + package.json | 2 +- yarn.lock | 72 +- 26 files changed, 657 insertions(+), 683 deletions(-) create mode 100644 examples/tutorial/index.html delete mode 100644 examples/tutorial/public/index.html delete mode 100644 examples/tutorial/public/manifest.json create mode 100644 examples/tutorial/src/App.css rename examples/tutorial/src/{App.js => App.tsx} (50%) delete mode 100644 examples/tutorial/src/Dashboard.js create mode 100644 examples/tutorial/src/Dashboard.tsx create mode 100644 examples/tutorial/src/MyUrlField.tsx create mode 100644 examples/tutorial/src/assets/react.svg rename examples/tutorial/src/{authProvider.js => authProvider.ts} (87%) delete mode 100644 examples/tutorial/src/index.js delete mode 100644 examples/tutorial/src/index.module.css delete mode 100644 examples/tutorial/src/logo.svg create mode 100644 examples/tutorial/src/main.tsx delete mode 100644 examples/tutorial/src/posts.js create mode 100644 examples/tutorial/src/posts.tsx delete mode 100644 examples/tutorial/src/registerServiceWorker.js rename examples/tutorial/src/{users.js => users.tsx} (56%) create mode 100644 examples/tutorial/src/vite-env.d.ts create mode 100644 examples/tutorial/tsconfig.json create mode 100644 examples/tutorial/tsconfig.node.json create mode 100644 examples/tutorial/vite.config.ts diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 8f213fdc08b..4f6a92634be 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -13,20 +13,22 @@ Here is an overview of the result: ## Setting Up -React-admin uses React. We'll use [create-react-app](https://github.com/facebookincubator/create-react-app) to create an empty React app, and install the `react-admin` package: +React-admin uses React. We'll use [vite.js](https://vitejs.dev/) to create an empty React app, and install the `react-admin` package: ```sh -yarn create react-app test-admin +yarn create vite test-admin --template react-ts cd test-admin/ -yarn add react-admin ra-data-json-server prop-types -yarn start +yarn add react-admin ra-data-json-server +yarn dev ``` -You should be up and running with an empty React application on port 3000. +You should be up and running with an empty React application on port 5173. + +**Tip**: Although this tutorial uses a TypeScript template, you can use react-admin with JavaScript if you prefer. Also, you can use [create-react-app](./CreateReactApp.md), [Next.js](./NextJs.md), [Remix](./Remix.md), or any other React framework to create your admin app. React-admin is framework agnostic. ## Using an API As Data Source -React-admin runs in the browser, and relies on data it fetches from APIs. +React-admin runs in the browser, and fetches data from an APIs. We'll be using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a fake REST API designed for testing and prototyping, as the datasource for the application. Here is what it looks like: @@ -64,13 +66,12 @@ JSONPlaceholder provides endpoints for users, posts, and comments. The admin we' ## Making Contact With The API Using a Data Provider -Bootstrap the admin app by replacing the `src/App.js` by the following code: +Bootstrap the admin app by replacing the `src/App.tsx` by the following code: ```jsx -// in src/App.js -import * as React from "react"; -import { Admin } from 'react-admin'; -import jsonServerProvider from 'ra-data-json-server'; +// in src/App.tsx +import { Admin } from "react-admin"; +import jsonServerProvider from "ra-data-json-server"; const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com'); @@ -79,6 +80,22 @@ const App = () => ; export default App; ``` +Also, remove the default Vite CSS fom the `main.tsx` file: + +```diff +// in src/main.tsx +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +-import './index.css' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) +``` + That's enough for react-admin to render an empty app and confirm that the setup is done: [![Empty Admin](./img/tutorial_empty.png)](./img/tutorial_empty.png) @@ -91,22 +108,21 @@ Now it's time to add features! We'll start by adding a list of users. -The `` component expects one or more `` child components. Each resource maps a name to an endpoint in the API. Edit the `App.js` file to add a resource named `users`: +The `` component expects one or more `` child components. Each resource maps a name to an endpoint in the API. Edit the `App.tsx` file to add a resource named `users`: ```diff -// in src/App.js -import * as React from "react"; --import { Admin } from 'react-admin'; -+import { Admin, Resource, ListGuesser } from 'react-admin'; -import jsonServerProvider from 'ra-data-json-server'; +// in src/App.tsx +-import { Admin } from "react-admin"; ++import { Admin, Resource, ListGuesser } from "react-admin"; +import jsonServerProvider from "ra-data-json-server"; const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com'); -const App = () => ; +const App = () => ( -+ -+ -+ ++ ++ ++ +); export default App; @@ -128,42 +144,44 @@ The `` component is not meant to be used in production - it's just [![Guessed Users List](./img/tutorial_guessed_list.png)](./img/tutorial_guessed_list.png) -Let's copy this code, and create a new `UserList` component, in a new file named `users.js`: +Let's copy this code, and create a new `UserList` component, in a new file named `users.tsx`: ```jsx -// in src/users.js -import * as React from "react"; -import { List, Datagrid, TextField, EmailField } from 'react-admin'; +// in src/users.tsx +import { List, Datagrid, TextField, EmailField } from "react-admin"; export const UserList = () => ( - - - - - - - - - - - - + + + + + + + + + + + + ); ``` -Then, edit the `App.js` file to use this new component instead of `ListGuesser`: +Then, edit the `App.tsx` file to use this new component instead of `ListGuesser`: ```diff -// in src/App.js --import { Admin, Resource, ListGuesser } from 'react-admin'; -+import { Admin, Resource } from 'react-admin'; -+import { UserList } from './users'; +// in src/App.tsx +-import { Admin, Resource, ListGuesser } from "react-admin"; ++import { Admin, Resource } from "react-admin"; +import jsonServerProvider from "ra-data-json-server"; ++import { UserList } from "./users"; + +const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com"); const App = () => ( - -- -+ - + +- ++ + ); ``` @@ -177,18 +195,18 @@ Let's take a moment to analyze the code of the `` component: ```jsx export const UserList = () => ( - - - - - - - - - - - - + + + + + + + + + + + + ); ``` @@ -205,18 +223,17 @@ But in most frameworks, "simple" means "limited", and it's hard to go beyond bas This means we can compose `` with another component - for instance ``: ```jsx -// in src/users.js -import * as React from "react"; -import { List, SimpleList } from 'react-admin'; +// in src/users.tsx +import { List, SimpleList } from "react-admin"; export const UserList = () => ( - - record.name} - secondaryText={record => record.username} - tertiaryText={record => record.email} - /> - + + record.name} + secondaryText={(record) => record.username} + tertiaryText={(record) => record.email} + /> + ); ``` @@ -239,36 +256,35 @@ But on desktop, `` takes too much space for a low information densit To do so, we'll use [the `useMediaQuery` hook](https://mui.com/material-ui/react-use-media-query/#main-content) from MUI: ```jsx -// in src/users.js -import * as React from "react"; -import { useMediaQuery } from '@mui/material'; -import { List, SimpleList, Datagrid, TextField, EmailField } from 'react-admin'; +// in src/users.tsx +import { useMediaQuery } from "@mui/material"; +import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; export const UserList = () => { - const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); - return ( - - {isSmall ? ( - record.name} - secondaryText={record => record.username} - tertiaryText={record => record.email} - /> - ) : ( - - - - - - - - - - - )} - - ); -} + const isSmall = useMediaQuery((theme) => theme.breakpoints.down("sm")); + return ( + + {isSmall ? ( + record.name} + secondaryText={(record) => record.username} + tertiaryText={(record) => record.email} + /> + ) : ( + + + + + + + + + + + )} + + ); +}; ``` This works exactly the way you expect. @@ -284,17 +300,17 @@ Let's get back to ``. It reads the data fetched by ``, then rend `` created one column for every field in the response. That's a bit too much for a usable grid, so let's remove a couple `` from the Datagrid and see the effect: ```diff -// in src/users.js - - - -- - -- - - - - +// in src/users.tsx + + + +- + +- + + + + ``` [![Users List](./img/tutorial_users_list_selected_columns.png)](./img/tutorial_users_list_selected_columns.png) @@ -308,20 +324,19 @@ You've just met the `` and the `` components. React-admin For instance, the `website` field looks like a URL. Instead of displaying it as text, why not display it using a clickable link? That's exactly what the `` does: ```diff -// in src/users.js --import { List, SimpleList, Datagrid, TextField, EmailField } from 'react-admin'; -+import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from 'react-admin'; +// in src/users.tsx +-import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; ++import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from "react-admin"; // ... - - - - - -- -+ - - - + + + + + +- ++ + + ``` [![Url Field](./img/tutorial_url_field.png)](./img/tutorial_url_field.png) @@ -335,19 +350,14 @@ In react-admin, fields are just React components. When rendered, they grab the ` That means that you can do the same to write a custom Field. For instance, here is a simplified version of the ``: ```jsx -// in src/MyUrlField.js -import * as React from "react"; -import { useRecordContext } from 'react-admin'; +// in src/MyUrlField.tsx +import { useRecordContext } from "react-admin"; const MyUrlField = ({ source }) => { - const record = useRecordContext(); - if (!record) return null; - return ( - - {record[source]} - - ); -} + const record = useRecordContext(); + if (!record) return null; + return {record[source]}; +}; export default MyUrlField; ``` @@ -357,20 +367,20 @@ For each row, `` creates a `RecordContext` and stores the current reco You can use the `` component in ``, instead of react-admin's `` component, and it will work just the same. ```diff -// in src/users.js --import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from 'react-admin'; -+import { List, SimpleList, Datagrid, TextField, EmailField } from 'react-admin'; +// in src/users.tsx +-import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from "react-admin"; ++import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; +import MyUrlField from './MyUrlField'; // ... - - - - - -- -+ - - + + + + + +- ++ + + ``` That means react-admin never blocks you: if one react-admin component doesn't perfectly suit your needs, you can just swap it with your own version. @@ -383,21 +393,20 @@ React-admin relies on [MUI](https://mui.com/), a set of React components modeled {% raw %} ```jsx -// in src/MyUrlField.js -import * as React from "react"; -import { useRecordContext } from 'react-admin'; -import { Link } from '@mui/material'; -import LaunchIcon from '@mui/icons-material/Launch'; - -const MyUrlField = ({ source }) => { - const record = useRecordContext(); - return record ? ( - - {record[source]} - - - ) : null; -} +// in src/MyUrlField.tsx +import { useRecordContext } from "react-admin"; +import { Link } from "@mui/material"; +import LaunchIcon from "@mui/icons-material/Launch"; + +const MyUrlField = ({ source }: { source: string }) => { + const record = useRecordContext(); + return record ? ( + + {record[source]} + + + ) : null; +}; export default MyUrlField; ``` @@ -427,18 +436,17 @@ In JSONPlaceholder, each `post` record includes a `userId` field, which points t React-admin knows how to take advantage of these foreign keys to fetch references. Let's see how the `ListGuesser` manages them by creating a new `` for the `/posts` API endpoint: ```diff -// in src/App.js -import * as React from "react"; --import { Admin, Resource } from 'react-admin'; -+import { Admin, Resource, ListGuesser } from 'react-admin'; -import jsonServerProvider from 'ra-data-json-server'; -import { UserList } from './users'; +// in src/App.tsx +-import { Admin, Resource } from "react-admin"; ++import { Admin, Resource, ListGuesser } from "react-admin"; +import jsonServerProvider from "ra-data-json-server"; +import { UserList } from "./users"; const App = () => ( - -+ - - + ++ + + ); export default App; @@ -449,28 +457,27 @@ export default App; The `ListGuesser` suggests using a `` for the `userId` field. Let's play with this new field by creating the `PostList` component based on the code dumped by the guesser: ```jsx -// in src/posts.js -import * as React from "react"; -import { List, Datagrid, TextField, ReferenceField } from 'react-admin'; +// in src/posts.tsx +import { List, Datagrid, TextField, ReferenceField } from "react-admin"; export const PostList = () => ( - - - - - - - - + + + + + + + + ); ``` ```diff -// in src/App.js --import { Admin, Resource, ListGuesser } from 'react-admin'; -+import { Admin, Resource } from 'react-admin'; -+import { PostList } from './posts'; -import { UserList } from './users'; +// in src/App.tsx +-import { Admin, Resource, ListGuesser } from "react-admin"; ++import { Admin, Resource } from "react-admin"; ++import { PostList } from "./posts"; +import { UserList } from "./users"; const App = () => ( @@ -484,7 +491,7 @@ const App = () => ( When displaying the posts list, the app displays the `id` of the post author. This doesn't mean much - we should use the user `name` instead. For that purpose, set the `recordRepresentation` prop of the "users" Resource: ```diff -// in src/App.js +// in src/App.tsx const App = () => ( @@ -505,23 +512,22 @@ The `` component fetches the reference data, creates a `RecordCo To finish the post list, place the post `id` field as first column, and remove the `body` field. From a UX point of view, fields containing large chunks of text should not appear in a Datagrid, only in detail views. Also, to make the Edit action stand out, let's replace the `rowClick` action by an explicit action button: ```diff -// in src/posts.js -import * as React from "react"; --import { List, Datagrid, TextField, ReferenceField } from 'react-admin'; -+import { List, Datagrid, TextField, ReferenceField, EditButton } from 'react-admin'; +// in src/posts.tsx +-import { List, Datagrid, TextField, ReferenceField } from "react-admin"; ++import { List, Datagrid, TextField, ReferenceField, EditButton } from "react-admin"; export const PostList = () => ( - -- -+ -+ - -- - -- -+ - - + +- ++ ++ + +- + +- ++ + + ); ``` @@ -532,11 +538,11 @@ export const PostList = () => ( An admin interface isn't just about displaying remote data, it should also allow editing records. React-admin provides an `` component for that purpose ; let's use the `` to help bootstrap it. ```diff -// in src/App.js --import { Admin, Resource } from 'react-admin'; -+import { Admin, Resource, EditGuesser } from 'react-admin'; -import { PostList } from './posts'; -import { UserList } from './users'; +// in src/App.tsx +-import { Admin, Resource } from "react-admin"; ++import { Admin, Resource, EditGuesser } from "react-admin"; +import { PostList } from "./posts"; +import { UserList } from "./users"; const App = () => ( @@ -551,56 +557,80 @@ const App = () => ( Users can display the edit page just by clicking on the Edit button. The form is already functional; it issues `PUT` requests to the REST API upon submission. And thanks to the `recordRepresentation` of the "users" Resource, the user name is displayed for the post author. -Copy the `` code dumped by the guesser in the console to the `posts.js` file so that you can customize the view: +Copy the `` code dumped by the guesser in the console to the `posts.tsx` file so that you can customize the view: ```jsx -// in src/posts.js -import * as React from "react"; +// in src/posts.tsx import { - List, - Datagrid, - TextField, - ReferenceField, - EditButton, - Edit, - SimpleForm, - ReferenceInput, - TextInput, -} from 'react-admin'; + List, + Datagrid, + TextField, + ReferenceField, + EditButton, + Edit, + SimpleForm, + ReferenceInput, + TextInput, +} from "react-admin"; -export const PostList = props => ( +export const PostList = () => ( { /* ... */ } ); export const PostEdit = () => ( - - - - - - - - + + + + + + + + +); +``` + +Use that component as the `edit` prop of the "posts" Resource instead of the guesser: + +```diff +// in src/App.tsx +-import { Admin, Resource, EditGuesser } from "react-admin"; ++import { Admin, Resource } from "react-admin"; +import jsonServerProvider from "ra-data-json-server"; +-import { PostList } from "./posts"; ++import { PostList, PostEdit } from "./posts"; +import { UserList } from "./users"; + +const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com"); + +const App = () => ( + +- ++ + + ); ``` You can now adjust the `` component to disable the edition of the primary key (`id`), place it first, and use a longer text input for the `body` field, as follows: +{% raw %} ```diff -// in src/posts.js +// in src/posts.tsx export const PostEdit = () => ( - - -+ - -- - -- -+ - - + + ++ + +- +- ++ +- ++ + + ); ``` +{% endraw %} If you've understood the `` component, the `` component will be no surprise. It's responsible for fetching the record, and displaying the page title. It passes the record down to the `` component, which is responsible for the form layout, default values, and validation. Just like ``, `` uses its children to determine the form inputs to display. It expects *input components* as children. `` and `` are such inputs. @@ -610,9 +640,9 @@ The `` takes the same props as the `` (used earl Let's allow users to create posts, too. Copy the `` component into a ``, and replace `` by ``: +{% raw %} ```diff -// in src/posts.js -import * as React from "react"; +// in src/posts.tsx import { List, Datagrid, @@ -624,51 +654,51 @@ import { SimpleForm, ReferenceInput, TextInput, -} from 'react-admin'; +} from "react-admin"; -export const PostList = props => ( - // ... +export const PostList = () => ( + { /* ... */ } ); -export const PostEdit = props => ( - // ... +export const PostEdit = () => ( + { /* ... */ } ); -+export const PostCreate = props => ( -+ -+ -+ -+ -+ -+ -+ ++export const PostCreate = () => ( ++ ++ ++ ++ ++ ++ ++ +); ``` +{% endraw %} **Tip**: The `` and the `` components use almost the same child form, except for the additional `id` input in ``. In most cases, the forms for creating and editing a record are a bit different, because most APIs create primary keys server-side. But if the forms are the same, you can share a common form component in `` and ``. -To use the new `` and `` components in the posts resource, just add them as `edit` and `create` attributes in the `` component: +To use the new `` components in the posts resource, just add it as `create` attribute in the `` component: ```diff -// in src/App.js --import { Admin, Resource, EditGuesser } from 'react-admin'; -+import { Admin, Resource } from 'react-admin'; --import { PostList } from './posts'; -+import { PostList, PostEdit, PostCreate } from './posts'; -import { UserList } from './users'; +// in src/App.tsx +import { Admin, Resource } from "react-admin"; +-import { PostList, PostEdit } from "./posts"; ++import { PostList, PostEdit, PostCreate } from "./posts"; +import { UserList } from "./users"; const App = () => ( - -- -+ - - + +- ++ + + ); ``` [![Post Creation](./img/tutorial_post_create.gif)](./img/tutorial_post_create.gif) -React-admin automatically adds a "create" button on top of the posts list to give access to the `` component. And the creation form works ; it issues a `POST` request to the REST API upon submission. +React-admin automatically adds a "create" button on top of the posts list to give access to the `create` component. And the creation form works ; it issues a `POST` request to the REST API upon submission. ## Optimistic Rendering And Undo @@ -695,10 +725,14 @@ The post editing page has a slight problem: it uses the post id as main title (t Let's customize the view title with a custom title component: ```diff -// in src/posts.js +// in src/posts.tsx ++import { useRecordContext} from "react-admin"; + +// ... + +const PostTitle = () => { -+ const record = useRecordContext(); -+ return Post {record ? `"${record.title}"` : ''}; ++ const record = useRecordContext(); ++ return Post {record ? `"${record.title}"` : ''}; +}; export const PostEdit = () => ( @@ -722,9 +756,7 @@ Let's get back to the post list for a minute. It offers sorting and pagination, React-admin can use Input components to create a multi-criteria search engine in the list view. Pass an array of such Input components to the List `filters` prop to enable filtering: ```jsx -// in src/posts.js -import { ReferenceInput, TextInput, List } from 'react-admin'; - +// in src/posts.tsx const postFilters = [ , , @@ -743,22 +775,22 @@ The first filter, 'q', takes advantage of a full-text functionality offered by J Filters are "search-as-you-type", meaning that when the user enters new values in the filter form, the list refreshes (via an API request) immediately. -**Tip**: Note that the `label` property can be used on any field to customize the field label. +**Tip**: Note that the `label` property can be used on any input to customize its label. ## Customizing the Menu Icons The sidebar menu shows the same icon for both posts and users. Customizing the menu icon is just a matter of passing an `icon` attribute to each ``: ```jsx -// in src/App.js -import PostIcon from '@mui/icons-material/Book'; -import UserIcon from '@mui/icons-material/Group'; +// in src/App.tsx +import PostIcon from "@mui/icons-material/Book"; +import UserIcon from "@mui/icons-material/Group"; const App = () => ( - - - - + + + + ); ``` @@ -769,28 +801,25 @@ const App = () => ( By default, react-admin displays the list page of the first `Resource` element as home page. If you want to display a custom component instead, pass it in the `dashboard` prop of the `` component. ```jsx -// in src/Dashboard.js -import * as React from "react"; -import { Card, CardContent, CardHeader } from '@mui/material'; - -const Dashboard = () => ( - - - Lorem ipsum sic dolor amet... - +// in src/Dashboard.tsx +import { Card, CardContent, CardHeader } from "@mui/material"; + +export const Dashboard = () => ( + + + Lorem ipsum sic dolor amet... + ); - -export default Dashboard; ``` ```jsx -// in src/App.js -import Dashboard from './Dashboard'; +// in src/App.tsx +import { Dashboard } from './Dashboard'; const App = () => ( - - // ... - + + // ... + ); ``` @@ -807,38 +836,36 @@ For this tutorial, since there is no public authentication API, we can use a fak The `authProvider` must expose 5 methods, each returning a `Promise`: ```jsx -// in src/authProvider.js -const authProvider = { - // called when the user attempts to log in - login: ({ username }) => { - localStorage.setItem('username', username); - // accept all username/password combinations - return Promise.resolve(); - }, - // called when the user clicks on the logout button - logout: () => { - localStorage.removeItem('username'); - return Promise.resolve(); - }, - // called when the API returns an error - checkError: ({ status }) => { - if (status === 401 || status === 403) { - localStorage.removeItem('username'); - return Promise.reject(); - } - return Promise.resolve(); - }, - // called when the user navigates to a new location, to check for authentication - checkAuth: () => { - return localStorage.getItem('username') - ? Promise.resolve() - : Promise.reject(); - }, - // called when the user navigates to a new location, to check for permissions / roles - getPermissions: () => Promise.resolve(), +// in src/authProvider.ts +export const authProvider = { + // called when the user attempts to log in + login: ({ username }: { username: string }) => { + localStorage.setItem("username", username); + // accept all username/password combinations + return Promise.resolve(); + }, + // called when the user clicks on the logout button + logout: () => { + localStorage.removeItem("username"); + return Promise.resolve(); + }, + // called when the API returns an error + checkError: ({ status }: { status: number }) => { + if (status === 401 || status === 403) { + localStorage.removeItem("username"); + return Promise.reject(); + } + return Promise.resolve(); + }, + // called when the user navigates to a new location, to check for authentication + checkAuth: () => { + return localStorage.getItem("username") + ? Promise.resolve() + : Promise.reject(); + }, + // called when the user navigates to a new location, to check for permissions / roles + getPermissions: () => Promise.resolve(), }; - -export default authProvider; ``` **Tip**: As the `authProvider` calls are asynchronous, you can easily fetch an authentication server in there. @@ -846,12 +873,12 @@ export default authProvider; To enable this authentication strategy, pass the `authProvider` to the `` component: ```jsx -// in src/App.js -import Dashboard from './Dashboard'; -import authProvider from './authProvider'; +// in src/App.tsx +import { Dashboard } from './Dashboard'; +import { authProvider } from './authProvider'; const App = () => ( - + // ... ); @@ -887,13 +914,13 @@ React-admin calls the Data Provider with one method for each of the actions of t The code for a Data Provider for the `my.api.url` API is as follows: ```js -import { fetchUtils } from 'react-admin'; +import { fetchUtils } from "react-admin"; import { stringify } from 'query-string'; const apiUrl = 'https://my.api.com/'; const httpClient = fetchUtils.fetchJson; -const dataProvider= { +export const dataProvider= { getList: (resource, params) => { const { page, perPage } = params.pagination; const { field, order } = params.sort; @@ -980,8 +1007,6 @@ const dataProvider= { }).then(({ json }) => ({ data: json })); } }; - -export default dataProvider; ``` **Tip**: `fetchUtils.fetchJson()` is just a shortcut for `fetch().then(r => r.json())`, plus a control of the HTTP response code to throw an `HTTPError` in case of 4xx or 5xx response. Feel free to use `fetch()` directly if it doesn't suit your needs. @@ -989,13 +1014,13 @@ export default dataProvider; Using this provider instead of the previous `jsonServerProvider` is just a matter of switching a function: ```jsx -// in src/app.js -import dataProvider from './dataProvider'; +// in src/app.tsx +import { dataProvider } from './dataProvider'; const App = () => ( - - // ... - + + // ... + ); ``` diff --git a/examples/tutorial/index.html b/examples/tutorial/index.html new file mode 100644 index 00000000000..3773aaed425 --- /dev/null +++ b/examples/tutorial/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json index c2d3ac3d676..2489004a3e8 100644 --- a/examples/tutorial/package.json +++ b/examples/tutorial/package.json @@ -1,25 +1,24 @@ { - "name": "tutorial", - "version": "4.0.0", - "private": true, - "dependencies": { - "@mui/icons-material": "^5.0.1", - "@mui/material": "^5.0.2", - "ra-data-json-server": "^4.0.0", - "react": "^17.0.0", - "react-admin": "^4.0.0", - "react-dom": "^17.0.0", - "react-scripts": "^5.0.0" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "eject": "react-scripts eject" - }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ] + "name": "tutorial", + "version": "4.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "ra-data-json-server": "^4.5.0", + "react": "^18.2.0", + "react-admin": "^4.5.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.22", + "@types/react-dom": "^18.0.7", + "@vitejs/plugin-react": "^2.2.0", + "typescript": "^4.6.4", + "vite": "^3.2.0" + } } diff --git a/examples/tutorial/public/index.html b/examples/tutorial/public/index.html deleted file mode 100644 index ed0ebafa1b7..00000000000 --- a/examples/tutorial/public/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - React App - - - -
- - - diff --git a/examples/tutorial/public/manifest.json b/examples/tutorial/public/manifest.json deleted file mode 100644 index ef19ec243e7..00000000000 --- a/examples/tutorial/public/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": "./index.html", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/examples/tutorial/src/App.css b/examples/tutorial/src/App.css new file mode 100644 index 00000000000..2c5e2ef5cd1 --- /dev/null +++ b/examples/tutorial/src/App.css @@ -0,0 +1,41 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/tutorial/src/App.js b/examples/tutorial/src/App.tsx similarity index 50% rename from examples/tutorial/src/App.js rename to examples/tutorial/src/App.tsx index e029454e3c7..be9b899bcdf 100644 --- a/examples/tutorial/src/App.js +++ b/examples/tutorial/src/App.tsx @@ -1,32 +1,35 @@ -import * as React from 'react'; +import { Admin, Resource } from 'react-admin'; +import jsonServerProvider from 'ra-data-json-server'; import PostIcon from '@mui/icons-material/Book'; import UserIcon from '@mui/icons-material/Group'; -import { Admin, Resource, ListGuesser } from 'react-admin'; -import jsonServerProvider from 'ra-data-json-server'; -import { PostList, PostEdit, PostCreate, PostShow } from './posts'; +import { PostList, PostEdit, PostCreate } from './posts'; import { UserList } from './users'; -import Dashboard from './Dashboard'; -import authProvider from './authProvider'; +import { Dashboard } from './Dashboard'; +import { authProvider } from './authProvider'; + +const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com'); const App = () => ( + - - ); + export default App; diff --git a/examples/tutorial/src/Dashboard.js b/examples/tutorial/src/Dashboard.js deleted file mode 100644 index 11df2736ddb..00000000000 --- a/examples/tutorial/src/Dashboard.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; - -export default () => ( - - - Lorem ipsum sic dolor amet... - -); diff --git a/examples/tutorial/src/Dashboard.tsx b/examples/tutorial/src/Dashboard.tsx new file mode 100644 index 00000000000..8fcb264825c --- /dev/null +++ b/examples/tutorial/src/Dashboard.tsx @@ -0,0 +1,8 @@ +import { Card, CardContent, CardHeader } from '@mui/material'; + +export const Dashboard = () => ( + + + Lorem ipsum sic dolor amet... + +); diff --git a/examples/tutorial/src/MyUrlField.tsx b/examples/tutorial/src/MyUrlField.tsx new file mode 100644 index 00000000000..dc4d6bf02e2 --- /dev/null +++ b/examples/tutorial/src/MyUrlField.tsx @@ -0,0 +1,15 @@ +import { useRecordContext } from 'react-admin'; +import { Link } from '@mui/material'; +import LaunchIcon from '@mui/icons-material/Launch'; + +const MyUrlField = ({ source }: { source: string }) => { + const record = useRecordContext(); + return record ? ( + + {record[source]} + + + ) : null; +}; + +export default MyUrlField; diff --git a/examples/tutorial/src/assets/react.svg b/examples/tutorial/src/assets/react.svg new file mode 100644 index 00000000000..6c87de9bb33 --- /dev/null +++ b/examples/tutorial/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tutorial/src/authProvider.js b/examples/tutorial/src/authProvider.ts similarity index 87% rename from examples/tutorial/src/authProvider.js rename to examples/tutorial/src/authProvider.ts index 79187965b8f..8d632129589 100644 --- a/examples/tutorial/src/authProvider.js +++ b/examples/tutorial/src/authProvider.ts @@ -1,6 +1,6 @@ -export default { +export const authProvider = { // called when the user attempts to log in - login: ({ username }) => { + login: ({ username }: { username: string }) => { localStorage.setItem('username', username); // accept all username/password combinations return Promise.resolve(); @@ -11,7 +11,7 @@ export default { return Promise.resolve(); }, // called when the API returns an error - checkError: ({ status }) => { + checkError: ({ status }: { status: number }) => { if (status === 401 || status === 403) { localStorage.removeItem('username'); return Promise.reject(); diff --git a/examples/tutorial/src/index.js b/examples/tutorial/src/index.js deleted file mode 100644 index 5dd3a1ba343..00000000000 --- a/examples/tutorial/src/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import './index.module.css'; -import App from './App'; -import registerServiceWorker from './registerServiceWorker'; - -ReactDOM.render(, document.getElementById('root')); -registerServiceWorker(); diff --git a/examples/tutorial/src/index.module.css b/examples/tutorial/src/index.module.css deleted file mode 100644 index 22f8f189710..00000000000 --- a/examples/tutorial/src/index.module.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; -} diff --git a/examples/tutorial/src/logo.svg b/examples/tutorial/src/logo.svg deleted file mode 100644 index 6b60c1042f5..00000000000 --- a/examples/tutorial/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/examples/tutorial/src/main.tsx b/examples/tutorial/src/main.tsx new file mode 100644 index 00000000000..03a1ea7d9ef --- /dev/null +++ b/examples/tutorial/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +); diff --git a/examples/tutorial/src/posts.js b/examples/tutorial/src/posts.js deleted file mode 100644 index ae5fab349a0..00000000000 --- a/examples/tutorial/src/posts.js +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { - Show, - ShowButton, - SimpleShowLayout, - RichTextField, - DateField, - List, - Edit, - Create, - Datagrid, - ReferenceField, - TextField, - EditButton, - ReferenceInput, - SelectInput, - SimpleForm, - TextInput, -} from 'react-admin'; - -const postFilters = [ - , - - - , -]; - -export const PostList = props => ( - - - - - - - - - - - -); - -const PostTitle = ({ record }) => { - return Post {record ? `"${record.title}"` : ''}; -}; - -export const PostEdit = () => ( - }> - - - - - - - - - -); - -export const PostCreate = () => ( - - - - - - - - - -); - -export const PostShow = props => ( - - - - - - - - -); diff --git a/examples/tutorial/src/posts.tsx b/examples/tutorial/src/posts.tsx new file mode 100644 index 00000000000..464938f1c55 --- /dev/null +++ b/examples/tutorial/src/posts.tsx @@ -0,0 +1,55 @@ +import { + List, + Datagrid, + TextField, + ReferenceField, + EditButton, + Edit, + Create, + SimpleForm, + ReferenceInput, + TextInput, + useRecordContext, +} from 'react-admin'; + +const postFilters = [ + , + , +]; + +export const PostList = () => ( + + + + + + + + +); + +const PostTitle = () => { + const record = useRecordContext(); + return Post {record ? `"${record.title}"` : ''}; +}; + +export const PostEdit = () => ( + }> + + + + + + + +); + +export const PostCreate = () => ( + + + + + + + +); diff --git a/examples/tutorial/src/registerServiceWorker.js b/examples/tutorial/src/registerServiceWorker.js deleted file mode 100644 index 0bb0b00404f..00000000000 --- a/examples/tutorial/src/registerServiceWorker.js +++ /dev/null @@ -1,120 +0,0 @@ -// In production, we register a service worker to serve assets from local cache. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on the "N+1" visit to a page, since previously -// cached resources are updated in the background. - -// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. -// This link also includes instructions on opting out of this behavior. - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export default function register() { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Lets check if a service worker still exists or not. - checkValidServiceWorker(swUrl); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://goo.gl/SC7cgQ' - ); - }); - } else { - // Is not local host. Just register service worker - registerValidSW(swUrl); - } - }); - } -} - -function registerValidSW(swUrl) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the old content will have been purged and - // the fresh content will have been added to the cache. - // It's the perfect time to display a "New content is - // available; please refresh." message in your web app. - console.log( - 'New content is available; please refresh.' - ); - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - if ( - response.status === 404 || - response.headers.get('content-type').indexOf('javascript') === - -1 - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -} diff --git a/examples/tutorial/src/users.js b/examples/tutorial/src/users.tsx similarity index 56% rename from examples/tutorial/src/users.js rename to examples/tutorial/src/users.tsx index 17784698efe..039dba74fa1 100644 --- a/examples/tutorial/src/users.js +++ b/examples/tutorial/src/users.tsx @@ -1,12 +1,11 @@ -import * as React from 'react'; import { useMediaQuery } from '@mui/material'; -import { SimpleList, List, Datagrid, EmailField, TextField } from 'react-admin'; - -export const UserList = props => { - const isSmall = useMediaQuery(theme => theme.breakpoints.down('md')); +import { List, SimpleList, Datagrid, TextField, EmailField } from 'react-admin'; +import MyUrlField from './MyUrlField'; +export const UserList = () => { + const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); return ( - + {isSmall ? ( record.name} @@ -14,11 +13,13 @@ export const UserList = props => { tertiaryText={record => record.email} /> ) : ( - + - + + + )} diff --git a/examples/tutorial/src/vite-env.d.ts b/examples/tutorial/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/examples/tutorial/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/tutorial/tsconfig.json b/examples/tutorial/tsconfig.json new file mode 100644 index 00000000000..3d0a51a86e2 --- /dev/null +++ b/examples/tutorial/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/tutorial/tsconfig.node.json b/examples/tutorial/tsconfig.node.json new file mode 100644 index 00000000000..9d31e2aed93 --- /dev/null +++ b/examples/tutorial/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/tutorial/vite.config.ts b/examples/tutorial/vite.config.ts new file mode 100644 index 00000000000..d7ec70e1126 --- /dev/null +++ b/examples/tutorial/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/package.json b/package.json index 2a48b65c5d3..3b97828d7f3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "prettier": "prettier --config ./.prettierrc.js --write --list-different \"packages/*/src/**/*.{js,json,ts,tsx,css,md}\" \"examples/*/src/**/*.{js,ts,json,tsx,css,md}\" \"cypress/**/*.{js,ts,json,tsx,css,md}\"", "run-simple": "cd examples/simple && yarn start", "run-no-code": "cd examples/no-code && yarn dev", - "run-tutorial": "cd examples/tutorial && yarn start", + "run-tutorial": "cd examples/tutorial && yarn dev", "run-demo": "cd examples/demo && cross-env REACT_APP_DATA_PROVIDER=rest yarn dev", "build-demo": "cd examples/demo && cross-env REACT_APP_DATA_PROVIDER=rest yarn build", "run-graphql-demo": "cd examples/demo && cross-env REACT_APP_DATA_PROVIDER=graphql yarn dev", diff --git a/yarn.lock b/yarn.lock index f91b9467cf3..5475b5a9ebb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7017,6 +7017,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18.0.7": + version: 18.0.8 + resolution: "@types/react-dom@npm:18.0.8" + dependencies: + "@types/react": "*" + checksum: e5e18d30a272b799e7579929b27f8cd078c5ee92cc11a4be80b63d9f4330d22c97cc2aeb89945fc8e88d59a92438fa89f37ac92021665021988ed56cb864b424 + languageName: node + linkType: hard + "@types/react-is@npm:^16.7.1 || ^17.0.0": version: 17.0.3 resolution: "@types/react-is@npm:17.0.3" @@ -7076,6 +7085,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.0.22": + version: 18.0.24 + resolution: "@types/react@npm:18.0.24" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 8ea907a8f944421a120f4db9fe52fe3752837e11031ff16b866e835adebb09b2b2ca3ac5f8aa622683778f6d40f791a369c9008c45537c1a7eb64347648fae3c + languageName: node + linkType: hard + "@types/recharts@npm:^1.8.10": version: 1.8.23 resolution: "@types/recharts@npm:1.8.23" @@ -21860,7 +21880,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-json-server@^4.0.0, ra-data-json-server@workspace:packages/ra-data-json-server": +"ra-data-json-server@^4.5.0, ra-data-json-server@workspace:packages/ra-data-json-server": version: 0.0.0-use.local resolution: "ra-data-json-server@workspace:packages/ra-data-json-server" dependencies: @@ -22392,6 +22412,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a + languageName: node + linkType: hard + "react-draggable@npm:^4.4.3": version: 4.4.4 resolution: "react-draggable@npm:4.4.4" @@ -22873,6 +22905,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8 + languageName: node + linkType: hard + "read-cmd-shim@npm:^3.0.0": version: 3.0.0 resolution: "read-cmd-shim@npm:3.0.0" @@ -23880,6 +23921,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: b777f7ca0115e6d93e126ac490dbd82642d14983b3079f58f35519d992fa46260be7d6e6cede433a92db70306310c6f5f06e144f0e40c484199e09c1f7be53dd + languageName: node + linkType: hard + "schema-utils@npm:2.7.0": version: 2.7.0 resolution: "schema-utils@npm:2.7.0" @@ -25857,13 +25907,15 @@ __metadata: version: 0.0.0-use.local resolution: "tutorial@workspace:examples/tutorial" dependencies: - "@mui/icons-material": ^5.0.1 - "@mui/material": ^5.0.2 - ra-data-json-server: ^4.0.0 - react: ^17.0.0 - react-admin: ^4.0.0 - react-dom: ^17.0.0 - react-scripts: ^5.0.0 + "@types/react": ^18.0.22 + "@types/react-dom": ^18.0.7 + "@vitejs/plugin-react": ^2.2.0 + ra-data-json-server: ^4.5.0 + react: ^18.2.0 + react-admin: ^4.5.0 + react-dom: ^18.2.0 + typescript: ^4.6.4 + vite: ^3.2.0 languageName: unknown linkType: soft @@ -25974,7 +26026,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^3 || ^4, typescript@npm:^4.4.0": +"typescript@npm:^3 || ^4, typescript@npm:^4.4.0, typescript@npm:^4.6.4": version: 4.8.4 resolution: "typescript@npm:4.8.4" bin: @@ -25984,7 +26036,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.4.0#~builtin": +"typescript@patch:typescript@^3 || ^4#~builtin, typescript@patch:typescript@^4.4.0#~builtin, typescript@patch:typescript@^4.6.4#~builtin": version: 4.8.4 resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=493e53" bin: From ccdef3c91fe724c00d415617f2e8987fc6f251aa Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Fri, 4 Nov 2022 11:10:21 +0100 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Jean-Baptiste Kaiser --- docs/Tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 4f6a92634be..e7a0adc1ac0 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -28,7 +28,7 @@ You should be up and running with an empty React application on port 5173. ## Using an API As Data Source -React-admin runs in the browser, and fetches data from an APIs. +React-admin runs in the browser, and fetches data from an API. We'll be using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a fake REST API designed for testing and prototyping, as the datasource for the application. Here is what it looks like: From 653b672a1fdbb19e2c1870fd1eb1afe3f7aaee0c Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 4 Nov 2022 11:11:16 +0100 Subject: [PATCH 3/6] Review --- docs/Tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index e7a0adc1ac0..492e43f5c5f 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -915,7 +915,7 @@ The code for a Data Provider for the `my.api.url` API is as follows: ```js import { fetchUtils } from "react-admin"; -import { stringify } from 'query-string'; +import { stringify } from "query-string"; const apiUrl = 'https://my.api.com/'; const httpClient = fetchUtils.fetchJson; From c79224259b8de885b91a9d1ea726022f0dd1eceb Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 4 Nov 2022 15:42:33 +0100 Subject: [PATCH 4/6] Fix warnings --- docs/Tutorial.md | 15 +++++---------- examples/tutorial/src/posts.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 492e43f5c5f..6b0b6a0f94d 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -611,9 +611,8 @@ const App = () => ( ); ``` -You can now adjust the `` component to disable the edition of the primary key (`id`), place it first, and use a longer text input for the `body` field, as follows: +You can now adjust the `` component to disable the edition of the primary key (`id`), place it first, and use a textarea for the `body` field, as follows: -{% raw %} ```diff // in src/posts.tsx export const PostEdit = () => ( @@ -622,15 +621,13 @@ export const PostEdit = () => ( + - -- -+ + - -+ ++
); ``` -{% endraw %} If you've understood the `` component, the `` component will be no surprise. It's responsible for fetching the record, and displaying the page title. It passes the record down to the `` component, which is responsible for the form layout, default values, and validation. Just like ``, `` uses its children to determine the form inputs to display. It expects *input components* as children. `` and `` are such inputs. @@ -640,7 +637,6 @@ The `` takes the same props as the `` (used earl Let's allow users to create posts, too. Copy the `` component into a ``, and replace `` by ``: -{% raw %} ```diff // in src/posts.tsx import { @@ -668,13 +664,12 @@ export const PostEdit = () => ( + + + -+ -+ ++ ++ + + +); ``` -{% endraw %} **Tip**: The `` and the `` components use almost the same child form, except for the additional `id` input in ``. In most cases, the forms for creating and editing a record are a bit different, because most APIs create primary keys server-side. But if the forms are the same, you can share a common form component in `` and ``. diff --git a/examples/tutorial/src/posts.tsx b/examples/tutorial/src/posts.tsx index 464938f1c55..f73cfec28b0 100644 --- a/examples/tutorial/src/posts.tsx +++ b/examples/tutorial/src/posts.tsx @@ -38,8 +38,8 @@ export const PostEdit = () => ( - - + + ); @@ -48,8 +48,8 @@ export const PostCreate = () => ( - - + + ); From 5123443d463fb6f3ff263b254bd43e26f0211d68 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Fri, 4 Nov 2022 18:19:21 +0100 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: AnĂ­bal Svarcas --- docs/Tutorial.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 6b0b6a0f94d..989c0a592f5 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -24,13 +24,13 @@ yarn dev You should be up and running with an empty React application on port 5173. -**Tip**: Although this tutorial uses a TypeScript template, you can use react-admin with JavaScript if you prefer. Also, you can use [create-react-app](./CreateReactApp.md), [Next.js](./NextJs.md), [Remix](./Remix.md), or any other React framework to create your admin app. React-admin is framework agnostic. +**Tip**: Although this tutorial uses a TypeScript template, you can use react-admin with JavaScript if you prefer. Also, you can use [create-react-app](./CreateReactApp.md), [Next.js](./NextJs.md), [Remix](./Remix.md), or any other React framework to create your admin app. React-admin is framework-agnostic. ## Using an API As Data Source React-admin runs in the browser, and fetches data from an API. -We'll be using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a fake REST API designed for testing and prototyping, as the datasource for the application. Here is what it looks like: +We'll be using [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a fake REST API designed for testing and prototyping, as the data source for the application. Here is what it looks like: ``` curl https://jsonplaceholder.typicode.com/users/2 @@ -398,7 +398,7 @@ import { useRecordContext } from "react-admin"; import { Link } from "@mui/material"; import LaunchIcon from "@mui/icons-material/Launch"; -const MyUrlField = ({ source }: { source: string }) => { +const MyUrlField = ({ source }) => { const record = useRecordContext(); return record ? ( @@ -834,7 +834,7 @@ The `authProvider` must expose 5 methods, each returning a `Promise`: // in src/authProvider.ts export const authProvider = { // called when the user attempts to log in - login: ({ username }: { username: string }) => { + login: ({ username }) => { localStorage.setItem("username", username); // accept all username/password combinations return Promise.resolve(); @@ -845,7 +845,7 @@ export const authProvider = { return Promise.resolve(); }, // called when the API returns an error - checkError: ({ status }: { status: number }) => { + checkError: ({ status }) => { if (status === 401 || status === 403) { localStorage.removeItem("username"); return Promise.reject(); From 788c774f1395e19cacafa39b1401ac961c3e0502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Mon, 7 Nov 2022 18:40:41 +0100 Subject: [PATCH 6/6] =?UTF-8?q?=C2=A3Fix=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Tutorial.md | 4 ++-- examples/tutorial/src/posts.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 989c0a592f5..15828bb046b 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -623,7 +623,7 @@ export const PostEdit = () => ( - - -+ ++ ); @@ -665,7 +665,7 @@ export const PostEdit = () => ( + + + -+ ++ + + +); diff --git a/examples/tutorial/src/posts.tsx b/examples/tutorial/src/posts.tsx index f73cfec28b0..4ffc9ef8655 100644 --- a/examples/tutorial/src/posts.tsx +++ b/examples/tutorial/src/posts.tsx @@ -39,7 +39,7 @@ export const PostEdit = () => ( - +
); @@ -49,7 +49,7 @@ export const PostCreate = () => ( - + );