documentation/versioned_docs/version-4.xx.xx/data/packages/strapi-v4/index.md
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
import axios from "axios";
const axiosInstance = axios.create();
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer 6ae3cf664d558bc67d21ddabd0cf5ba0716367cd74c2ceaedf86f0efa09b3fe1605c90ab051fd4961ba03db961273bb2b48b9213ae267013317977f737b4ac8765a2e0bc64e9f275791ccb881117553f589675f5e6ce84d3859511fa124d477209cf1cbbd4fd7f6ddacc77eb4520753e3636446f807629de911eac7afbf60fd4`;
Refine supports the features that come with Strapi-v4.
A few of the Strapi-v4 API features are as follows:
meta allows us to use the above features in hooks. Thus, we can fetch the data according to the parameters we want.
Hooks and components that support meta:
:::note
There is no need to use meta for sorting, pagination, and, filters. Sorting, pagination, and, filters will be handled automatically by the strapi-v4 dataProvider.
:::
:::info
Normally, strapi-v4 backend returns data in the following format:
{
"id": 1,
"attributes": {
"title": "My title",
"content": "Long content...",
}
However, we can use normalizeData to customize the data returned by the backend. So, our data will look like:
{
"id": 1,
"title": "My title",
"content": "Long content..."
}
:::
:::caution
To make this example more visual, we used the @refinedev/antd package. If you are using Refine headless, you need to provide the components, hooks, or helpers imported from the @refinedev/antd package.
:::
import { Refine } from "@refinedev/core";
//highlight-next-line
import { DataProvider } from "@refinedev/strapi-v4";
const App: React.FC = () => {
return (
<Refine
//highlight-next-line
dataProvider={DataProvider("API_URL")}
/* ... */
>
</Refine>
);
};
Let's examine how API parameters that come with Strapi-v4 are used with meta. Then, let's see how it is used in the application.
We created two collections on Strapi as posts and categories and added a relation between them. For detailed information on how to create a collection, you can check here.
<Tabs defaultValue="posts" values={[ {label: 'posts', value: 'posts'}, {label: 'categories', value: 'categories'} ]}> <TabItem value="posts">
posts has the following fields:
idtitlecontentcategorycreatedAtlocalecategories has the following fields:
idtitleTo select only some fields, we must specify these fields with `meta``.
Refer to the Fields Selection documentation for detailed information. →
const { tableProps } = useTable<IPost>({
meta: {
fields: ["id", "title"],
},
});
const { tableProps } = useTable<IPost>({
meta: {
fields: "*",
},
});
When sending the request, we can specify which fields will come, so we send fields in meta to hooks that we will fetch data from. In this way, you can perform the queries of only the fields you want.
setInitialRoutes(["/posts"]);
import { Refine } from "@refinedev/core";
import { ThemedLayoutV2, RefineThemes } from "@refinedev/antd";
import { ConfigProvider, Layout } from "antd";
import routerProvider from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { DataProvider } from "@refinedev/strapi-v4";
const API_URL = "https://api.strapi-v4.refine.dev";
// visible-block-start
// src/pages/posts/list.tsx
import { List, EditButton, ShowButton, useTable } from "@refinedev/antd";
import { Table, Space } from "antd";
const PostList = () => {
const { tableProps, sorter } = useTable<IPost>({
meta: {
// highlight-start
fields: ["id", "title"],
// highlight-end
},
});
return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
// visible-block-end
const App: React.FC = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={DataProvider(`${API_URL}/api`, axiosInstance)}
resources={[
{
name: "posts",
list: "/posts",
},
]}
>
<Routes>
<Route
element={
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
}
>
<Route path="posts">
<Route index element={<PostList />} />
</Route>
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
By default, relations are not populated when fetching entries.
The populate parameter is used to define which fields will be populated.
Refer to the Relations Population documentation for detailed information. →
const { tableProps } = useTable<IPost>({
meta: {
populate: ["category", "cover"],
},
});
const { tableProps } = useTable<IPost>({
meta: {
populate: "*",
},
});
It should be noted that Strapi-V4 allows populating relations more than 1 level.
const { tableProps } = useTable<IPost>({
meta: {
populate: {
category: {
populate: ["cover"],
},
cover: {
populate: [""],
},
},
},
});
In order to pull the categories related to the posts, we can now show the categories in our list by defining the meta populate parameter.
setInitialRoutes(["/posts"]);
import { Refine } from "@refinedev/core";
import { ThemedLayoutV2, RefineThemes } from "@refinedev/antd";
import { ConfigProvider, Layout } from "antd";
import routerProvider from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { DataProvider } from "@refinedev/strapi-v4";
const API_URL = "https://api.strapi-v4.refine.dev";
// visible-block-start
// src/pages/posts/list.tsx
import {
List,
EditButton,
ShowButton,
// highlight-start
useSelect,
FilterDropdown,
// highlight-end
useTable,
} from "@refinedev/antd";
import {
Table,
// highlight-next-line
Select,
Space,
} from "antd";
const PostList = () => {
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title"],
// highlight-next-line
populate: ["category"],
},
});
// highlight-start
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
});
// highlight-end
return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select
style={{ minWidth: 200 }}
mode="multiple"
placeholder="Select Category"
{...selectProps}
/>
</FilterDropdown>
)}
/>
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
// visible-block-end
const App: React.FC = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={DataProvider(`${API_URL}/api`, axiosInstance)}
resources={[
{
name: "posts",
list: "/posts",
},
]}
>
<Routes>
<Route
element={
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
}
>
<Route path="posts">
<Route index element={<PostList />} />
</Route>
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
/me requestIf you need to the population for the /me request you can use it like this in your authProvider.
const strapiAuthHelper = AuthHelper(API_URL + "/api");
strapiAuthHelper.me("token", {
meta: {
populate: ["role"],
},
});
:::note
The Draft & Publish feature should be enabled on Strapi.
:::
Refer to the Publication State documentation for detailed information. →
live: returns only published entries
preview: returns draft and published entries
const { tableProps } = useTable<IPost>({
meta: {
publicationState: "preview",
},
});
We can list the posts separately according to the published or draft information.
setInitialRoutes(["/posts"]);
import { Refine } from "@refinedev/core";
import { ThemedLayoutV2, RefineThemes } from "@refinedev/antd";
import { ConfigProvider, Layout } from "antd";
import routerProvider from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { DataProvider } from "@refinedev/strapi-v4";
const API_URL = "https://api.strapi-v4.refine.dev";
// visible-block-start
// src/pages/posts/list.tsx
import {
List,
EditButton,
ShowButton,
useSelect,
FilterDropdown,
useTable,
} from "@refinedev/antd";
import {
Table,
Space,
Select,
// highlight-start
Form,
Radio,
Tag,
// highlight-end
} from "antd";
const PostList = () => {
// highlight-next-line
const [publicationState, setPublicationState] = React.useState("live");
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title", "publishedAt"],
populate: ["category"],
publicationState,
},
});
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
});
return (
<List>
<Form
style={{
marginBottom: 16,
display: "flex",
justifyContent: "center",
gap: "16px",
}}
layout="inline"
initialValues={{
publicationState,
}}
>
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select
style={{ minWidth: 200 }}
mode="multiple"
placeholder="Select Category"
{...selectProps}
/>
</FilterDropdown>
)}
/>
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return (
<Tag color={value ? "green" : "blue"}>
{value ? "Published" : "Draft"}
</Tag>
);
}}
/>
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
// visible-block-end
const App: React.FC = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={DataProvider(`${API_URL}/api`, axiosInstance)}
resources={[
{
name: "posts",
list: "/posts",
},
]}
>
<Routes>
<Route
element={
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
}
>
<Route path="posts">
<Route index element={<PostList />} />
</Route>
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
:::tip
To fetch content for a locale, make sure it has been already added to Strapi in the admin panel
:::
Refer to the Locale documentation for detailed information. →
const { tableProps } = useTable<IPost>({
meta: {
locale: "de",
},
});
With the local parameter feature, we can fetch posts and categories created according to different languages.
import { useState } from "react";
import {
List,
useTable,
getDefaultSortOrder,
FilterDropdown,
useSelect,
EditButton,
DeleteButton,
} from "@refinedev/antd";
import { Table, Select, Space, Form, Radio, Tag } from "antd";
import { IPost } from "interfaces";
import { API_URL } from "../../constants";
export const PostList: React.FC = () => {
//highlight-start
const [locale, setLocale] = useState("en");
//highlight-end
const [publicationState, setPublicationState] = useState("live");
const { tableProps, sorter } = useTable<IPost>({
meta: {
populate: ["category", "cover"],
//highlight-start
locale,
//highlight-end
publicationState,
},
});
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
//highlight-start
meta: { locale },
//highlight-end
});
return (
<List>
<Form
layout="inline"
//highlight-start
initialValues={{
locale,
publicationState,
}}
//highlight-end
>
//highlight-start
<Form.Item label="Locale" name="locale">
<Radio.Group onChange={(e) => setLocale(e.target.value)}>
<Radio.Button value="en">English</Radio.Button>
<Radio.Button value="de">Deutsch</Radio.Button>
</Radio.Group>
</Form.Item>
//highlight-end
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table
{...tableProps}
rowKey="id"
pagination={{
...tableProps.pagination,
showSizeChanger: true,
}}
>
<Table.Column
dataIndex="id"
title="ID"
defaultSortOrder={getDefaultSortOrder("id", sorter)}
sorter={{ multiple: 3 }}
/>
<Table.Column
dataIndex="title"
title="Title"
defaultSortOrder={getDefaultSortOrder("title", sorter)}
sorter={{ multiple: 2 }}
/>
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select
style={{ minWidth: 200 }}
mode="multiple"
placeholder="Select Category"
{...selectProps}
/>
</FilterDropdown>
)}
/>
<Table.Column dataIndex="locale" title="Locale" />
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return (
<Tag color={value ? "green" : "blue"}>
{value ? "Published" : "Draft"}
</Tag>
);
}}
/>
<Table.Column<{ id: string }>
title="Actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<DeleteButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
setInitialRoutes(["/posts"]);
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { DataProvider } from "@refinedev/strapi-v4";
const API_URL = "https://api.strapi-v4.refine.dev";
import { ConfigProvider, Layout } from "antd";
import { ThemedLayoutV2, RefineThemes } from "@refinedev/antd";
// visible-block-start
// src/pages/posts/list.tsx
import {
List,
EditButton,
ShowButton,
useSelect,
FilterDropdown,
useTable,
} from "@refinedev/antd";
import { Table, Space, Select, Form, Radio, Tag } from "antd";
const PostList = () => {
// highlight-next-line
const [locale, setLocale] = React.useState("en");
const [publicationState, setPublicationState] = React.useState("live");
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title", "publishedAt", "locale"],
populate: ["category"],
locale,
publicationState,
},
});
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
// highlight-next-line
meta: { locale },
});
return (
<List>
<Form
style={{
marginBottom: 16,
display: "flex",
justifyContent: "center",
gap: "16px",
}}
layout="inline"
initialValues={{
// highlight-next-line
locale,
publicationState,
}}
>
<Form.Item label="Locale" name="locale">
<Radio.Group onChange={(e) => setLocale(e.target.value)}>
<Radio.Button value="en">English</Radio.Button>
<Radio.Button value="de">Deutsch</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select
style={{ minWidth: 200 }}
mode="multiple"
placeholder="Select Category"
{...selectProps}
/>
</FilterDropdown>
)}
/>
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return (
<Tag color={value ? "green" : "blue"}>
{value ? "Published" : "Draft"}
</Tag>
);
}}
/>
<Table.Column dataIndex="locale" title="Locale" />
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
// visible-block-end
const App: React.FC = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={DataProvider(`${API_URL}/api`, axiosInstance)}
resources={[
{
name: "posts",
list: "/posts",
},
]}
>
<Routes>
<Route
element={
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
}
>
<Route path="posts">
<Route index element={<PostList />} />
</Route>
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
meta UsagesWhen creating and editing posts you can use these API parameters in meta:
const { formProps, saveButtonProps, query } = useForm<IPost>({
meta: { publicationState: "preview" },
});
const { formProps, saveButtonProps, query } = useForm<IPost>({
meta: { populate: ["category", "cover"] },
});
const { selectProps } = useSelect({
meta: { locale: "en" },
});
Strapi V4 supports authentication and you can use the Refine's authProvider to add authentication to your application.
First, we need to create own fetch instance to use both authProvider and dataProvider. We will use axios to create the fetch instance, however you can use any other library for this.
import axios from "axios";
export const axiosInstance = axios.create();
Then, we need to give the axiosInstance to the dataProvider. Create a dataProvider with the axiosInstance and export it.
import { DataProvider } from "@refinedev/strapi-v4";
import { axiosInstance } from "./utils/axios";
import { API_URL } from "./constants";
export const dataProvider = DataProvider(`${API_URL}/api`, axiosInstance);
Now, we are ready to create the authProvider.
import { AuthHelper } from "@refinedev/strapi-v4";
import { type AuthProvider } from "@refinedev/core";
import { axiosInstance } from "./utils/axios";
import { TOKEN_KEY } from "./constants";
const authProvider: AuthProvider = {
login: async ({ email, password }) => {
try {
const { data, status } = await strapiAuthHelper.login(email, password);
if (status === 200) {
localStorage.setItem(TOKEN_KEY, data.jwt);
// set header axios instance
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer ${data.jwt}`;
return {
success: true,
redirectTo: "/",
};
}
} catch (error: any) {
const errorObj = error?.response?.data?.message?.[0]?.messages?.[0];
return {
success: false,
error: {
message: errorObj?.message || "Login failed",
name: errorObj?.id || "Invalid email or password",
},
};
}
return {
success: false,
error: {
message: "Login failed",
name: "Invalid email or password",
},
};
},
logout: async () => {
localStorage.removeItem(TOKEN_KEY);
axiosInstance.defaults.headers.common["Authorization"] = undefined;
return {
success: true,
redirectTo: "/login",
};
},
onError: async (error) => {
if (error.response?.status === 401) {
return {
logout: true,
};
}
return { error };
},
check: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer ${token}`;
return {
authenticated: true,
};
}
return {
authenticated: false,
error: {
message: "Authentication failed",
name: "Token not found",
},
logout: true,
redirectTo: "/login",
};
},
getPermissions: async () => null,
getIdentity: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (!token) {
return null;
}
const { data, status } = await strapiAuthHelper.me(token);
if (status === 200) {
const { id, username, email } = data;
return {
id,
username,
email,
};
}
return null;
},
};
After creating the authProvider, and dataProvider, we need to pass it to the Refine component.
import { Refine } from "@refinedev/core";
import { authProvider } from "./authProvider";
import { dataProvider } from "./dataProvider";
const App = () => {
return <Refine authProvider={authProvider} dataProvider={dataProvider} />;
};
This is the setup for authentication with Refine. Please refer to the Authentication guide for more information about how to use authentication with Refine.
Strapi supports file upload. Below are examples of how to upload files to Strapi.
Refer to the Strapi documentation for more information →
<Tabs defaultValue="antd" values={[ {label: 'Ant Design Form', value: 'antd'}, {label: 'React Hook Form', value: 'react-hook-form'}, {label: 'Mantine Form', value: 'mantine'} ]}> <TabItem value="antd">
getValueProps and mediaUploadMapper are helper functions for Ant Design Form.
import { Edit, useForm } from "@refinedev/antd";
import { getValueProps, mediaUploadMapper } from "@refinedev/strapi-v4";
import { Form, Upload } from "antd";
import { TOKEN_KEY, API_URL } from "../../constants";
import { IPost } from "../interfaces";
export const PostEdit: React.FC = () => {
const { formProps, saveButtonProps } = useForm<IPost>({
metaData: { populate: ["cover"] },
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Form
{...formProps}
layout="vertical"
onFinish={(values) => {
formProps.onFinish?.(mediaUploadMapper(values));
}}
>
<Form.Item label="Cover">
<Form.Item
name="cover"
valuePropName="fileList"
getValueProps={(data) => getValueProps(data, API_URL)}
noStyle
>
<Upload.Dragger
name="files"
action={`${API_URL}/api/upload`}
headers={{
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
}}
listType="picture"
multiple
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Edit>
);
};
import { useState } from "react";
import axios from "axios";
import { Edit } from "@refinedev/mui";
import { Box, Input, Stack, Typography } from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { HttpError } from "@refinedev/core";
import { useForm } from "@refinedev/react-hook-form";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { ICategory, IPost } from "interfaces";
import { TOKEN_KEY, API_URL } from "../../constants";
export const PostEdit: React.FC = () => {
const [isUploadLoading, setIsUploadLoading] = useState(false);
const [imageURL, setImageURL] = useState("");
const {
saveButtonProps,
register,
formState: { errors },
setValue,
setError,
} = useForm<IPost, HttpError, IPost & { category: ICategory; cover: any }>();
const onChangeHandler = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
try {
setIsUploadLoading(true);
const formData = new FormData();
const target = event.target;
const file: File = (target.files as FileList)[0];
formData.append("files", file);
const res = await axios.post(`${API_URL}/api/upload`, formData, {
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
},
});
setImageURL(`${API_URL}${res.data[0].url}`);
setValue("cover", res.data[0].id, { shouldValidate: true });
setIsUploadLoading(false);
} catch (error) {
setError("cover", { message: "Upload failed. Please try again." });
setIsUploadLoading(false);
}
};
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<Stack
direction="row"
gap={4}
flexWrap="wrap"
sx={{ marginTop: "16px" }}
>
<label htmlFor="images-input">
<Input
id="images-input"
type="file"
sx={{ display: "none" }}
onChange={onChangeHandler}
/>
<input
id="file"
{...register("cover", {
required: "This field is required",
})}
type="hidden"
/>
<LoadingButton
loading={isUploadLoading}
loadingPosition="end"
endIcon={<FileUploadIcon />}
variant="contained"
component="span"
>
Upload
</LoadingButton>
{errors.cover && (
<Typography variant="caption" color="#fa541c">
{errors.cover?.message?.toString()}
</Typography>
)}
</label>
{imageURL && (
<Box
component="img"
sx={{
maxWidth: 250,
maxHeight: 250,
}}
src={imageURL}
alt="Post image"
/>
)}
</Stack>
</Box>
</Edit>
);
};
import { useState } from "react";
import axios from "axios";
import { Edit, useForm } from "@refinedev/mantine";
import { Text } from "@mantine/core";
import { Dropzone, IMAGE_MIME_TYPE, FileWithPath } from "@mantine/dropzone";
const API_URL = "http://localhost:1337";
const TOKEN_KEY = "strapi-jwt-token";
export const PostEdit: React.FC = () => {
const [isUploadLoading, setIsUploadLoading] = useState(false);
const { saveButtonProps, setFieldValue } = useForm<any>({
initialValues: {
title: "",
cover: "",
},
});
const handleOnDrop = async (files: FileWithPath[]) => {
try {
setIsUploadLoading(true);
const formData = new FormData();
const file = files[0];
formData.append("files", file);
const res = await axios.post(`${API_URL}/api/upload`, formData, {
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
},
});
setFieldValue("cover", res.data[0].id);
setIsUploadLoading(false);
} catch (error) {
setIsUploadLoading(false);
}
};
return (
<Edit saveButtonProps={saveButtonProps}>
<form>
<Text mt={8} weight={500} size="sm" color="#212529">
Cover
</Text>
<Dropzone
accept={IMAGE_MIME_TYPE}
onDrop={handleOnDrop}
loading={isUploadLoading}
>
<Text align="center">Drop images here</Text>
</Dropzone>
</form>
</Edit>
);
};
Strapi provides a way to add validation rules to your models. So if you send a request to the server with invalid data, Strapi will return errors for each field that has a validation error.
Refer to the Strapi documentation for more information →
By default, @refinedev/strapi-v4 transforms the error response from Strapi into a HttpError object. This object contains the following properties:
statusCode - The status code of the response.message - The error message.errors - An object containing the validation errors for each field.Thus, useForm will automatically set the error message for each field that has a validation error.
Refer to the server-side form validation documentation for more information → .
:::note Demo Credentials
Username: [email protected]
Password: demodemo
:::
<CodeSandboxExample path="data-provider-strapi-v4" />