Merge branch 'main' into features/deits

This commit is contained in:
Ben Irvin 2023-01-18 10:58:13 +01:00 committed by GitHub
commit e76735c829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 120 additions and 26 deletions

View File

@ -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")`.

View File

@ -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',
}
```

View File

@ -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';

View File

@ -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')

View File

@ -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.

View File

@ -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();

View File

@ -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';

View File

@ -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();

View File

@ -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', () => {

View File

@ -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;

View File

@ -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');
}
});
});

View File

@ -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);