Merge pull request #16804 from strapi/chore/refactor-use-content-types-hook

Chore: Refactor useModels to use react-query rather than useReducer
This commit is contained in:
Gustav Hansen 2023-05-23 13:44:50 +02:00 committed by GitHub
commit e576dae3be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 199 additions and 248 deletions

View File

@ -1,5 +1,5 @@
export { default as useConfigurations } from './useConfigurations';
export { default as useModels } from './useModels';
export { useContentTypes } from './useContentTypes';
export { default as useFetchPermissionsLayout } from './useFetchPermissionsLayout';
export { default as useFetchRole } from './useFetchRole';
export { default as useMenu } from './useMenu';

View File

@ -0,0 +1 @@
export * from './useContentTypes';

View File

@ -0,0 +1,142 @@
import * as React from 'react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { renderHook } from '@testing-library/react-hooks';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { useContentTypes } from '../useContentTypes';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn().mockReturnValue(jest.fn),
}));
const server = setupServer(
rest.get('*/content-manager/content-types', (req, res, ctx) =>
res(
ctx.json({
data: [
{
uid: 'admin::collectionType',
isDisplayed: true,
apiID: 'permission',
kind: 'collectionType',
},
{
uid: 'admin::collectionTypeNotDispalyed',
isDisplayed: false,
apiID: 'permission',
kind: 'collectionType',
},
{
uid: 'admin::singleType',
isDisplayed: true,
kind: 'singleType',
},
{
uid: 'admin::singleTypeNotDispalyed',
isDisplayed: false,
kind: 'singleType',
},
],
})
)
),
rest.get('*/content-manager/components', (req, res, ctx) =>
res(
ctx.json({
data: [
{
uid: 'basic.relation',
isDisplayed: true,
apiID: 'relation',
category: 'basic',
info: {
displayName: 'Relation',
},
options: {},
attributes: {
id: {
type: 'integer',
},
categories: {
type: 'relation',
relation: 'oneToMany',
target: 'api::category.category',
targetModel: 'api::category.category',
relationType: 'oneToMany',
},
},
},
],
})
)
)
);
const setup = () =>
renderHook(() => useContentTypes(), {
wrapper({ children }) {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
<QueryClientProvider client={client}>
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
</QueryClientProvider>
);
},
});
describe('useContentTypes', () => {
beforeAll(() => {
server.listen();
});
afterAll(() => {
server.close();
});
test('fetches models and content-types', async () => {
const { result, waitFor } = setup();
expect(result.current.isLoading).toBe(true);
expect(result.current.components).toStrictEqual([]);
expect(result.current.singleTypes).toStrictEqual([]);
expect(result.current.collectionTypes).toStrictEqual([]);
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.components).toStrictEqual(
expect.arrayContaining([
expect.objectContaining({
uid: 'basic.relation',
}),
])
);
expect(result.current.collectionTypes).toStrictEqual([
expect.objectContaining({
uid: 'admin::collectionType',
}),
]);
expect(result.current.singleTypes).toStrictEqual([
expect.objectContaining({
uid: 'admin::singleType',
}),
]);
});
});

View File

@ -0,0 +1,45 @@
import { useAPIErrorHandler, useFetchClient, useNotification } from '@strapi/helper-plugin';
import { useQueries } from 'react-query';
export function useContentTypes() {
const { get } = useFetchClient();
const { formatAPIError } = useAPIErrorHandler();
const toggleNotification = useNotification();
const queries = useQueries(
['components', 'content-types'].map((type) => {
return {
queryKey: ['content-manager', type],
async queryFn() {
const {
data: { data },
} = await get(`/content-manager/${type}`);
return data;
},
onError(error) {
toggleNotification({
type: 'warning',
message: formatAPIError(error),
});
},
};
})
);
const [components, contentTypes] = queries;
const isLoading = components.isLoading || contentTypes.isLoading;
const collectionTypes = (contentTypes?.data ?? []).filter(
(contentType) => contentType.kind === 'collectionType' && contentType.isDisplayed
);
const singleTypes = (contentTypes?.data ?? []).filter(
(contentType) => contentType.kind !== 'collectionType' && contentType.isDisplayed
);
return {
isLoading,
components: components?.data ?? [],
collectionTypes,
singleTypes,
};
}

View File

@ -1,58 +0,0 @@
import { useReducer, useEffect, useCallback } from 'react';
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer';
/**
* TODO: refactor this to not use the `useReducer` hook,
* it's not really necessary. Also use `useQuery`?
*/
const useModels = () => {
const toggleNotification = useNotification();
const [state, dispatch] = useReducer(reducer, initialState);
const { get } = useFetchClient();
const fetchModels = useCallback(async () => {
dispatch({
type: 'GET_MODELS',
});
try {
const [
{
data: { data: components },
},
{
data: { data: contentTypes },
},
] = await Promise.all(
['components', 'content-types'].map((endPoint) => get(`/content-manager/${endPoint}`))
);
dispatch({
type: 'GET_MODELS_SUCCEDED',
contentTypes,
components,
});
} catch (err) {
dispatch({
type: 'GET_MODELS_ERROR',
});
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
}, [toggleNotification, get]);
useEffect(() => {
fetchModels();
}, [fetchModels]);
return {
...state,
getData: fetchModels,
};
};
export default useModels;

View File

@ -1,45 +0,0 @@
/* eslint-disable consistent-return */
import produce from 'immer';
export const initialState = {
collectionTypes: [],
components: [],
isLoading: true,
singleTypes: [],
};
const reducer = (state, action) =>
produce(state, (draftState) => {
switch (action.type) {
case 'GET_MODELS': {
draftState.collectionTypes = initialState.collectionTypes;
draftState.singleTypes = initialState.singleTypes;
draftState.components = initialState.components;
draftState.isLoading = true;
break;
}
case 'GET_MODELS_ERROR': {
draftState.collectionTypes = initialState.collectionTypes;
draftState.singleTypes = initialState.singleTypes;
draftState.components = initialState.components;
draftState.isLoading = false;
break;
}
case 'GET_MODELS_SUCCEDED': {
const getContentTypeByKind = (kind) =>
action.contentTypes.filter(
(contentType) => contentType.isDisplayed && contentType.kind === kind
);
draftState.isLoading = false;
draftState.collectionTypes = getContentTypeByKind('collectionType');
draftState.singleTypes = getContentTypeByKind('singleType');
draftState.components = action.components;
break;
}
default:
return draftState;
}
});
export default reducer;

View File

@ -1,132 +0,0 @@
import reducer from '../reducer';
describe('ADMIN | HOOKS | useModels | reducer', () => {
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
const state = {
test: true,
};
expect(reducer(state, {})).toEqual(state);
});
});
describe('GET_MODELS_ERROR', () => {
it('should set isLoading to false is an error occured', () => {
const action = {
type: 'GET_MODELS_ERROR',
};
const initialState = {
collectionTypes: [],
components: [],
singleTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
kind: 'singleType',
},
],
isLoading: true,
};
const expected = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: false,
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
describe('GET_MODELS', () => {
it('should set isLoading to true to start getting the data', () => {
const action = {
type: 'GET_MODELS',
};
const initialState = {
collectionTypes: [
{
uid: 'app.category',
isDisplayed: true,
kind: 'collectionType',
},
{
uid: 'app.category',
isDisplayed: true,
kind: 'collectionType',
},
],
singleTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
kind: 'singleType',
},
],
components: [{}],
isLoading: false,
};
const expected = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: true,
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
describe('GET_MODELS_SUCCEDED', () => {
it('should return the state with the collectionTypes and singleTypes', () => {
const action = {
type: 'GET_MODELS_SUCCEDED',
contentTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
kind: 'singleType',
},
{
uid: 'permissions.role',
isDisplayed: false,
kind: 'collectionType',
},
{
uid: 'app.category',
isDisplayed: true,
kind: 'collectionType',
},
],
components: [],
};
const initialState = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: true,
};
const expected = {
collectionTypes: [
{
uid: 'app.category',
isDisplayed: true,
kind: 'collectionType',
},
],
singleTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
kind: 'singleType',
},
],
components: [],
isLoading: false,
};
expect(reducer(initialState, action)).toEqual(expected);
});
});
});

View File

@ -12,7 +12,7 @@ import { LoadingIndicatorPage, useGuidedTour } from '@strapi/helper-plugin';
import { Layout, Main, Box, Grid, GridItem } from '@strapi/design-system';
import useLicenseLimitNotification from 'ee_else_ce/hooks/useLicenseLimitNotification';
import cornerOrnamentPath from './assets/corner-ornament.svg';
import { useModels } from '../../hooks';
import { useContentTypes } from '../../hooks/useContentTypes';
import isGuidedTourCompleted from '../../components/GuidedTour/utils/isGuidedTourCompleted';
import GuidedTourHomepage from '../../components/GuidedTour/Homepage';
import SocialLinks from './SocialLinks';
@ -31,7 +31,7 @@ const LogoContainer = styled(Box)`
const HomePage = () => {
// Temporary until we develop the menu API
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useContentTypes();
const { guidedTourState, isGuidedTourVisible, isSkipped } = useGuidedTour();
useLicenseLimitNotification();

View File

@ -6,7 +6,7 @@ import { IntlProvider } from 'react-intl';
import { useAppInfo } from '@strapi/helper-plugin';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import HomePage from '../index';
import { useModels } from '../../../hooks';
import { useContentTypes } from '../../../hooks/useContentTypes';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
@ -30,9 +30,7 @@ jest.mock('@strapi/helper-plugin', () => ({
})),
}));
jest.mock('../../../hooks', () => ({
useModels: jest.fn(),
}));
jest.mock('../../../hooks/useContentTypes');
jest.mock('ee_else_ce/hooks/useLicenseLimitNotification', () => ({
__esModule: true,
@ -52,11 +50,11 @@ const App = (
);
describe('Homepage', () => {
useModels.mockImplementation(() => ({
useContentTypes.mockReturnValue({
isLoading: false,
collectionTypes: [],
singleTypes: [],
}));
});
test('should render all homepage links', () => {
const { getByRole } = render(App);
@ -113,11 +111,11 @@ describe('Homepage', () => {
});
it('should display particular text and action when there are collectionTypes and singletypes', () => {
useModels.mockImplementation(() => ({
useContentTypes.mockReturnValue({
isLoading: false,
collectionTypes: [{ uuid: 102 }],
singleTypes: [{ isDisplayed: true }],
}));
});
const { getByText, getByRole } = render(App);

View File

@ -14,7 +14,7 @@ import {
import { Main } from '@strapi/design-system';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { useModels } from '../../../../../hooks';
import { useContentTypes } from '../../../../../hooks/useContentTypes';
import WebhookForm from './components/WebhookForm';
import cleanData from './utils/formatData';
@ -27,7 +27,7 @@ const EditView = () => {
const { lockApp, unlockApp } = useOverlayBlocker();
const toggleNotification = useNotification();
const queryClient = useQueryClient();
const { isLoading: isLoadingForModels, collectionTypes } = useModels();
const { isLoading: isLoadingForModels, collectionTypes } = useContentTypes();
const { put, get, post } = useFetchClient();
const isCreating = id === 'create';