mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +00:00 
			
		
		
		
	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:
		
						commit
						e576dae3be
					
				| @ -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'; | ||||
|  | ||||
| @ -0,0 +1 @@ | ||||
| export * from './useContentTypes'; | ||||
| @ -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', | ||||
|       }), | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @ -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, | ||||
|   }; | ||||
| } | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -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(); | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Gustav Hansen
						Gustav Hansen