mirror of
https://github.com/strapi/strapi.git
synced 2025-10-17 11:08:14 +00:00
Merge branch 'main' into features/deits
This commit is contained in:
commit
e76735c829
@ -116,7 +116,7 @@ filter out the relation from the array of relations. This is handled inside the
|
||||
|
||||
:::note
|
||||
Connecting relations adds the item to the end of the list, whilst loading more relations prepends to
|
||||
the beginning of the list. This is the expected behaviour.
|
||||
the beginning of the list. This is the expected behaviour, to keep the order of the list in the UI in sync with the API response.
|
||||
:::
|
||||
|
||||
The `RelationInput` component takes the field in `modifiedData` as its source of truth. You could therefore consider this to
|
||||
@ -126,7 +126,7 @@ data for the api.
|
||||
|
||||
### Cleaning data to be posted to the API
|
||||
|
||||
The API to update the enttiy expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
|
||||
The API to update the entity expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
|
||||
You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three
|
||||
different arrays which makes the code more complex. Instead, because the browser doesn't really care about whats new and removed
|
||||
and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which
|
||||
@ -142,3 +142,85 @@ relations have been connected and which have been disconnected. Returning an obj
|
||||
```
|
||||
|
||||
## Frontend component architecture
|
||||
|
||||
The input field for relation fields consist of two components:
|
||||
|
||||
### `RelationInputDataManager`
|
||||
|
||||
This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from
|
||||
the `RelationInput` so that Strapi is able to move the underlying component into the design-system if the community would need it
|
||||
(most other input components can be consumed from there).
|
||||
|
||||
### `RelationInput`
|
||||
|
||||
This component is the presentational counterpart to the `RelationInputDataManager` component. It renders an input field based on the data passed from the data manager.
|
||||
|
||||
Under the hood it is using `react-window` to render a list of relations in a virtualized view. Some fields need to render thousands of relations, which
|
||||
would otherwise have a negative impact on the overall performance of the content-manager.
|
||||
|
||||
## useRelation() hook
|
||||
|
||||
This hook takes care of data-fetching and normalizes results relations aswell as search-results.
|
||||
|
||||
```ts
|
||||
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: string, options: Options);
|
||||
```
|
||||
|
||||
### `Options`
|
||||
|
||||
`option`s is a mandatory configuration and should implement the following shape:
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
name: string; // name of the relation field
|
||||
relation: RelationConfiguration;
|
||||
search: SearchConfiguration;
|
||||
}
|
||||
|
||||
type RelationConfiguration = {
|
||||
endpoint: string; // URL from where existing relations should be fetched
|
||||
enabled: boolean; // defines whether relations should be fetched once the hook is called
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
onLoad: (results: RelationResult[]) => void; // callback that will be fired after relations have been fetched (paginated)
|
||||
normalizeArguments = {
|
||||
mainFieldName: string; // name of the target model main field, determining which field to display (fallback: id)
|
||||
shouldAddLink: boolean; // if the user is allowed to read the target model, the returned relations should include a link to the target
|
||||
targetModel: object; // target content-type model
|
||||
};
|
||||
pageGoal: number; // the current page-count of the already loaded relations used to keep the redux store and query cache in sync.
|
||||
}
|
||||
|
||||
type SearchConfiguration = {
|
||||
endpoint: string; // URL from where new relations should be fetched
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
}
|
||||
```
|
||||
|
||||
### Return values
|
||||
|
||||
`relations` and `search` both return a consistent relation format:
|
||||
|
||||
```ts
|
||||
type RelationResults = RelationResult[];
|
||||
|
||||
type RelationResult = {
|
||||
id: number;
|
||||
href?: string; // based on `shouldAddLink` and the `targetModel`
|
||||
publicationState: 'draft' | 'published';
|
||||
mainField: string; // will fallback to "id" if not set
|
||||
}
|
||||
```
|
||||
|
||||
#### `relations`
|
||||
|
||||
`relations` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated relational data
|
||||
aswell as methods to check if there are more pages or fetch more paginated results. Relations for a given field are fetched as soon as the hook is called.
|
||||
|
||||
#### `search`
|
||||
|
||||
`search` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated search results
|
||||
for a relational field. Search results are only fetched after `searchFor()` has been called.
|
||||
|
||||
#### `searchFor(string)`
|
||||
|
||||
`searchFor` is a method which can be used to search for entities which haven't been connected with the source entity yet. The method accepts a search-term: `searchFor("term")`.
|
||||
|
@ -14,7 +14,7 @@ The following example shows a basic way to use the `useFetchClient` hook to make
|
||||
|
||||
```jsx
|
||||
import {useState} from "react"
|
||||
import useFetchClient from '@strapi/admin/admin/src/hooks/useFetchClient';
|
||||
import { useFetchClient } from '@strapi/helper-plugin';
|
||||
|
||||
const Component = () => {
|
||||
const [items, setItems] = useState([]);
|
||||
@ -57,7 +57,7 @@ The following information is the internal additions we've added to the axios ins
|
||||
|
||||
### Base URL
|
||||
|
||||
The default URL will be the one defined in the environment variable: `STRAPI_ADMIN_BACKEND_URL`.
|
||||
The default URL is the one defined in the _getFetchClient_ utility
|
||||
|
||||
### Interceptors
|
||||
|
||||
@ -68,8 +68,6 @@ The request interceptor adds the following parameters to the header:
|
||||
```js
|
||||
{
|
||||
Authorization: `Bearer <AUTH_TOKEN>`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -10,4 +10,3 @@ export { default as usePermissionsDataManager } from './usePermissionsDataManage
|
||||
export { default as useReleaseNotification } from './useReleaseNotification';
|
||||
export { default as useThemeToggle } from './useThemeToggle';
|
||||
export { default as useRegenerate } from './useRegenerate';
|
||||
export { default as useFetchClient } from './useFetchClient';
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
TrackingProvider,
|
||||
prefixFileUrlWithBackendUrl,
|
||||
useAppInfos,
|
||||
useFetchClient,
|
||||
} from '@strapi/helper-plugin';
|
||||
import axios from 'axios';
|
||||
import { SkipToContent } from '@strapi/design-system/Main';
|
||||
@ -25,7 +26,7 @@ import NotFoundPage from '../NotFoundPage';
|
||||
import UseCasePage from '../UseCasePage';
|
||||
import { getUID } from './utils';
|
||||
import routes from './utils/routes';
|
||||
import { useConfigurations, useFetchClient } from '../../hooks';
|
||||
import { useConfigurations } from '../../hooks';
|
||||
|
||||
const AuthenticatedApp = lazy(() =>
|
||||
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp')
|
||||
|
@ -3,3 +3,7 @@
|
||||
## Description
|
||||
|
||||
Helper to develop Strapi plugins.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [Contributing Guide](../../../CONTRIBUTING.md) before submitting a Pull Request to the project.
|
||||
|
@ -1,5 +1,10 @@
|
||||
/**
|
||||
*
|
||||
* useFetchClient
|
||||
*
|
||||
*/
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { getFetchClient } from '../../utils/getFetchClient';
|
||||
import getFetchClient from '../../utils/getFetchClient';
|
||||
|
||||
const useFetchClient = () => {
|
||||
const controller = useRef(null);
|
||||
@ -7,6 +12,7 @@ const useFetchClient = () => {
|
||||
if (controller.current === null) {
|
||||
controller.current = new AbortController();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
controller.current.abort();
|
@ -29,6 +29,7 @@ export { default as useRBAC } from './hooks/useRBAC';
|
||||
export { default as usePersistentState } from './hooks/usePersistentState';
|
||||
export { default as useFocusWhenNavigate } from './hooks/useFocusWhenNavigate';
|
||||
export { default as useLockScroll } from './hooks/useLockScroll';
|
||||
export { default as useFetchClient } from './hooks/useFetchClient';
|
||||
|
||||
// Providers
|
||||
export { default as GuidedTourProvider } from './providers/GuidedTourProvider';
|
||||
@ -100,3 +101,4 @@ export { default as wrapAxiosInstance } from './utils/wrapAxiosInstance';
|
||||
export { default as request } from './utils/request';
|
||||
export { default as getAPIInnerErrors } from './utils/getAPIInnerErrors';
|
||||
export { default as getYupInnerErrors } from './utils/getYupInnerErrors';
|
||||
export { default as getFetchClient } from './utils/getFetchClient';
|
||||
|
@ -1,11 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import auth from '../auth';
|
||||
|
||||
export const reqInterceptor = async (config) => {
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${auth.getToken()}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
return config;
|
||||
@ -33,13 +31,16 @@ export const addInterceptors = (instance) => {
|
||||
instance.interceptors.response.use(resInterceptor, resErrorInterceptor);
|
||||
};
|
||||
|
||||
export const fetchClient = ({ baseURL }) => {
|
||||
export const fetchClient = () => {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
addInterceptors(instance);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default fetchClient({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL });
|
||||
export default fetchClient();
|
@ -1,5 +1,5 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import auth from '../../auth';
|
||||
import {
|
||||
reqInterceptor,
|
||||
reqErrorInterceptor,
|
||||
@ -7,13 +7,13 @@ import {
|
||||
resErrorInterceptor,
|
||||
fetchClient,
|
||||
addInterceptors,
|
||||
} from '../fetchClient';
|
||||
} from '../index';
|
||||
|
||||
const token = 'coolToken';
|
||||
auth.getToken = jest.fn().mockReturnValue(token);
|
||||
auth.clearAppStorage = jest.fn().mockReturnValue(token);
|
||||
|
||||
describe('ADMIN | utils | fetchClient', () => {
|
||||
describe('HELPER-PLUGIN | utils | fetchClient', () => {
|
||||
describe('Test the interceptors', () => {
|
||||
it('API request should add authorization token to header', async () => {
|
||||
const apiInstance = fetchClient({
|
||||
@ -21,7 +21,6 @@ describe('ADMIN | utils | fetchClient', () => {
|
||||
});
|
||||
const result = await apiInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
|
||||
expect(result.headers.Authorization).toContain(`Bearer ${token}`);
|
||||
expect(result.headers.Accept).toBe('application/json');
|
||||
expect(apiInstance.interceptors.response.handlers[0].fulfilled('foo')).toBe('foo');
|
||||
});
|
||||
describe('Test the addInterceptor function', () => {
|
@ -1,6 +1,7 @@
|
||||
import instance from './fetchClient';
|
||||
import instance from '../fetchClient';
|
||||
|
||||
export const getFetchClient = (defaultOptions = {}) => {
|
||||
const getFetchClient = (defaultOptions = {}) => {
|
||||
instance.defaults.baseURL = window.strapi.backendURL;
|
||||
return {
|
||||
get: (url, config) => instance.get(url, { ...defaultOptions, ...config }),
|
||||
put: (url, data, config) => instance.put(url, data, { ...defaultOptions, ...config }),
|
||||
@ -8,3 +9,5 @@ export const getFetchClient = (defaultOptions = {}) => {
|
||||
del: (url, config) => instance.delete(url, { ...defaultOptions, ...config }),
|
||||
};
|
||||
};
|
||||
|
||||
export default getFetchClient;
|
@ -1,10 +1,10 @@
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import { getFetchClient } from '../getFetchClient';
|
||||
import auth from '../../auth';
|
||||
import getFetchClient from '../index';
|
||||
|
||||
const token = 'coolToken';
|
||||
auth.getToken = jest.fn().mockReturnValue(token);
|
||||
|
||||
describe('ADMIN | utils | getFetchClient', () => {
|
||||
describe('HELPER-PLUGIN | utils | getFetchClient', () => {
|
||||
it('should return the 4 HTTP methods to call GET, POST, PUT and DELETE apis', () => {
|
||||
const response = getFetchClient();
|
||||
expect(response).toHaveProperty('get');
|
||||
@ -19,7 +19,6 @@ describe('ADMIN | utils | getFetchClient', () => {
|
||||
} catch (err) {
|
||||
const { headers } = err.config;
|
||||
expect(headers.Authorization).toContain(`Bearer ${token}`);
|
||||
expect(headers.Accept).toBe('application/json');
|
||||
}
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ function wrapAxiosInstance(instance) {
|
||||
(methodName) => {
|
||||
wrapper[methodName] = (...args) => {
|
||||
console.log(
|
||||
'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the admin: { useFetchClient } from "@strapi/helper-plugin"'
|
||||
'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the helper-plugin: { useFetchClient } from "@strapi/helper-plugin"'
|
||||
);
|
||||
|
||||
return instance[methodName](...args);
|
||||
|
Loading…
x
Reference in New Issue
Block a user