diff --git a/packages/core/admin/admin/src/StrapiApp.js b/packages/core/admin/admin/src/StrapiApp.js index f693d621e8..7b533eac45 100644 --- a/packages/core/admin/admin/src/StrapiApp.js +++ b/packages/core/admin/admin/src/StrapiApp.js @@ -9,7 +9,7 @@ import { Helmet } from 'react-helmet'; import { BrowserRouter } from 'react-router-dom'; import Logo from './assets/images/logo-strapi-2022.svg'; -import localStorageKey from './components/LanguageProvider/utils/localStorageKey'; +import { LANGUAGE_LOCAL_STORAGE_KEY } from './components/LanguageProvider'; import Providers from './components/Providers'; import { customFields, Plugin } from './core/apis'; import configureStore from './core/store/configureStore'; @@ -462,7 +462,7 @@ class StrapiApp { href: this.configurations.head.favicon, }, ]} - htmlAttributes={{ lang: localStorage.getItem(localStorageKey) || 'en' }} + htmlAttributes={{ lang: localStorage.getItem(LANGUAGE_LOCAL_STORAGE_KEY) || 'en' }} /> diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js index 6256bb3ac7..bf9d59aac3 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js +++ b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js @@ -12,7 +12,7 @@ import { useQueries } from 'react-query'; // TODO: DS add loader import packageJSON from '../../../../package.json'; -import { useConfiguration } from '../../hooks'; +import { useConfiguration } from '../../hooks/useConfiguration'; import { getFullName, hashAdminUserEmail } from '../../utils'; import { NpsSurvey } from '../NpsSurvey'; import { PluginsInitializer } from '../PluginsInitializer'; diff --git a/packages/core/admin/admin/src/components/LanguageProvider.tsx b/packages/core/admin/admin/src/components/LanguageProvider.tsx new file mode 100644 index 0000000000..d647e9fae0 --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider.tsx @@ -0,0 +1,129 @@ +/* + * + * LanguageProvider + * + * this component connects the redux state language locale to the + * IntlProvider component and i18n messages (loaded from `app/translations`) + */ + +import * as React from 'react'; + +import defaultsDeep from 'lodash/defaultsDeep'; +import { IntlProvider } from 'react-intl'; + +/* ------------------------------------------------------------------------------------------------- + * LocalesContext + * -----------------------------------------------------------------------------------------------*/ + +interface LocalesContextValue { + changeLocale: (locale: keyof State['localeNames']) => void; + localeNames: Record; +} + +const LocalesContext = React.createContext({ + changeLocale: () => { + throw new Error('LocalesContext: changeLocale() is not implemented'); + }, + localeNames: {}, +}); + +const useLocales = () => React.useContext(LocalesContext); + +/* ------------------------------------------------------------------------------------------------- + * LanguageProvider + * -----------------------------------------------------------------------------------------------*/ + +const LANGUAGE_LOCAL_STORAGE_KEY = 'strapi-admin-language'; + +interface LanguageProviderProps { + children: React.ReactNode; + localeNames: Record; + messages: Record>; +} + +const LanguageProvider = ({ children, localeNames, messages }: LanguageProviderProps) => { + const [{ locale }, dispatch] = React.useReducer, State>( + reducer, + initialState, + () => { + const languageFromLocaleStorage = window.localStorage.getItem(LANGUAGE_LOCAL_STORAGE_KEY); + if (languageFromLocaleStorage && localeNames[languageFromLocaleStorage]) { + return { + locale: languageFromLocaleStorage, + localeNames, + }; + } else { + return { + locale: 'en', + localeNames, + }; + } + } + ); + + React.useEffect(() => { + // Set user language in local storage. + window.localStorage.setItem(LANGUAGE_LOCAL_STORAGE_KEY, locale); + document.documentElement.setAttribute('lang', locale); + }, [locale]); + + const changeLocale = React.useCallback((locale: keyof State['localeNames']) => { + dispatch({ + type: 'CHANGE_LOCALE', + locale, + }); + }, []); + + const appMessages = defaultsDeep(messages[locale], messages.en); + + const contextValue = React.useMemo( + () => ({ changeLocale, localeNames }), + [changeLocale, localeNames] + ); + + return ( + + {children} + + ); +}; + +/* ------------------------------------------------------------------------------------------------- + * Reducer + * -----------------------------------------------------------------------------------------------*/ + +interface State { + localeNames: Record; + locale: keyof State['localeNames']; +} + +const initialState: State = { + localeNames: { en: 'English' }, + locale: 'en', +}; + +interface ChangeLocaleAction { + type: 'CHANGE_LOCALE'; + locale: keyof State['localeNames']; +} + +type Action = ChangeLocaleAction; + +const reducer = (state = initialState, action: Action) => { + switch (action.type) { + case 'CHANGE_LOCALE': { + const { locale } = action; + + if (!state.localeNames[locale]) { + return state; + } + + return { ...state, locale }; + } + default: { + return state; + } + } +}; + +export { LanguageProvider, useLocales, LANGUAGE_LOCAL_STORAGE_KEY }; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/index.js b/packages/core/admin/admin/src/components/LanguageProvider/index.js deleted file mode 100644 index 1d069e5dfe..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/index.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * - * LanguageProvider - * - * this component connects the redux state language locale to the - * IntlProvider component and i18n messages (loaded from `app/translations`) - */ - -import React, { useEffect, useReducer } from 'react'; - -import defaultsDeep from 'lodash/defaultsDeep'; -import PropTypes from 'prop-types'; -import { IntlProvider } from 'react-intl'; - -import LocalesProvider from '../LocalesProvider'; - -import init from './init'; -import reducer, { initialState } from './reducer'; -import localStorageKey from './utils/localStorageKey'; - -const LanguageProvider = ({ children, localeNames, messages }) => { - const [{ locale }, dispatch] = useReducer(reducer, initialState, () => init(localeNames)); - - useEffect(() => { - // Set user language in local storage. - window.localStorage.setItem(localStorageKey, locale); - document.documentElement.setAttribute('lang', locale); - }, [locale]); - - const changeLocale = (locale) => { - dispatch({ - type: 'CHANGE_LOCALE', - locale, - }); - }; - - const appMessages = defaultsDeep(messages[locale], messages.en); - - return ( - - - {children} - - - ); -}; - -LanguageProvider.propTypes = { - children: PropTypes.element.isRequired, - localeNames: PropTypes.objectOf(PropTypes.string).isRequired, - messages: PropTypes.object.isRequired, -}; - -export default LanguageProvider; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/init.js b/packages/core/admin/admin/src/components/LanguageProvider/init.js deleted file mode 100644 index 54d4f8b7d4..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/init.js +++ /dev/null @@ -1,13 +0,0 @@ -import localStorageKey from './utils/localStorageKey'; - -const init = (localeNames) => { - const languageFromLocaleStorage = window.localStorage.getItem(localStorageKey); - const appLanguage = localeNames[languageFromLocaleStorage] ? languageFromLocaleStorage : 'en'; - - return { - locale: appLanguage, - localeNames, - }; -}; - -export default init; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/reducer.js b/packages/core/admin/admin/src/components/LanguageProvider/reducer.js deleted file mode 100644 index 6d2caec054..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/reducer.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * - * LanguageProvider reducer - * - */ - -const initialState = { - localeNames: { en: 'English' }, - locale: 'en', -}; - -const languageProviderReducer = (state = initialState, action) => { - switch (action.type) { - case 'CHANGE_LOCALE': { - const { locale } = action; - - if (!state.localeNames[locale]) { - return state; - } - - return { ...state, locale }; - } - default: { - return state; - } - } -}; - -export default languageProviderReducer; -export { initialState }; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js b/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js deleted file mode 100644 index 2e8cb1cbae..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import init from '../init'; - -const localeNames = { en: 'English', fr: 'Français' }; - -describe('LanguageProvider | init', () => { - afterEach(() => { - localStorage.removeItem('strapi-admin-language'); - }); - - it('should return the language from the localStorage', () => { - localStorage.setItem('strapi-admin-language', 'fr'); - - expect(init(localeNames)).toEqual({ - locale: 'fr', - localeNames, - }); - }); - - it('should return "en" when the strapi-admin-language is not set in the locale storage', () => { - expect(init(localeNames)).toEqual({ - locale: 'en', - localeNames, - }); - }); - - it('should return "en" when the language from the local storage is not included in the localeNames', () => { - localStorage.setItem('strapi-admin-language', 'foo'); - - expect(init(localeNames)).toEqual({ - locale: 'en', - localeNames, - }); - }); -}); diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js b/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js deleted file mode 100644 index 1dbdccce1d..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import reducer, { initialState } from '../reducer'; - -describe('LanguageProvider | reducer', () => { - let state; - - beforeEach(() => { - state = initialState; - }); - - it('should return the initialState', () => { - const action = { type: undefined }; - - expect(reducer(state, action)).toEqual(initialState); - }); - - it('should change the locale correctly when the locale is defined in the localeNames', () => { - state = { - localeNames: { en: 'English', fr: 'Français' }, - locale: 'en', - }; - - const action = { type: 'CHANGE_LOCALE', locale: 'fr' }; - const expected = { - localeNames: { en: 'English', fr: 'Français' }, - locale: 'fr', - }; - - expect(reducer(state, action)).toEqual(expected); - }); - - it('should not change the locale when the language is not defined in the localeNames', () => { - state = { - localeNames: { en: 'English', fr: 'Français' }, - locale: 'en', - }; - - const action = { type: 'CHANGE_LOCALE', locale: 'foo' }; - - expect(reducer(state, action)).toEqual(state); - }); -}); diff --git a/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js b/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js deleted file mode 100644 index 034ff886ca..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js +++ /dev/null @@ -1,3 +0,0 @@ -const localStorageKey = 'strapi-admin-language'; - -export default localStorageKey; diff --git a/packages/core/admin/admin/src/components/LeftMenu/index.js b/packages/core/admin/admin/src/components/LeftMenu.tsx similarity index 84% rename from packages/core/admin/admin/src/components/LeftMenu/index.js rename to packages/core/admin/admin/src/components/LeftMenu.tsx index 720d61c478..e07c5b95eb 100644 --- a/packages/core/admin/admin/src/components/LeftMenu/index.js +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import * as React from 'react'; import { Box, Divider, Flex, FocusTrap, Typography } from '@strapi/design-system'; import { @@ -19,12 +19,12 @@ import { useTracking, } from '@strapi/helper-plugin'; import { Exit, Write } from '@strapi/icons'; -import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { NavLink as RouterNavLink, useHistory, useLocation } from 'react-router-dom'; import styled from 'styled-components'; -import { useConfiguration } from '../../hooks'; +import { useConfiguration } from '../hooks/useConfiguration'; +import { Menu } from '../hooks/useMenu'; const LinkUserWrapper = styled(Box)` width: ${150 / 16}rem; @@ -33,7 +33,7 @@ const LinkUserWrapper = styled(Box)` left: ${({ theme }) => theme.spaces[5]}; `; -const LinkUser = styled(RouterNavLink)` +const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` display: flex; justify-content: space-between; align-items: center; @@ -54,9 +54,11 @@ const LinkUser = styled(RouterNavLink)` } `; -const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { - const buttonRef = useRef(); - const [userLinksVisible, setUserLinksVisible] = useState(false); +interface LeftMenuProps extends Pick {} + +const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => { + const navUserRef = React.useRef(null!); + const [userLinksVisible, setUserLinksVisible] = React.useState(false); const { logos: { menu }, } = useConfiguration(); @@ -83,16 +85,19 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { history.push('/auth/login'); }; - const handleBlur = (e) => { + const handleBlur: React.FocusEventHandler = (e) => { if ( !e.currentTarget.contains(e.relatedTarget) && + /** + * TODO: can we replace this by just using the navUserRef? + */ e.relatedTarget?.parentElement?.id !== 'main-nav-user-button' ) { setUserLinksVisible(false); } }; - const handleClickOnLink = (destination = null) => { + const handleClickOnLink = (destination: string) => { trackUsage('willNavigate', { from: pathname, to: destination }); }; @@ -126,6 +131,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { } onClick={() => handleClickOnLink('/content-manager')} @@ -146,6 +152,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { return ( } @@ -172,8 +179,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { 0 && link.notificationsCount.toString()) || undefined + link.notificationsCount && link.notificationsCount > 0 + ? link.notificationsCount.toString() + : undefined } + // @ts-expect-error the props from the passed as prop are not inferred // joined together to={link.to} key={link.to} icon={} @@ -190,7 +200,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { @@ -244,9 +254,4 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => { ); }; -LeftMenu.propTypes = { - generalSectionLinks: PropTypes.array.isRequired, - pluginsSectionLinks: PropTypes.array.isRequired, -}; - -export default LeftMenu; +export { LeftMenu }; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/context.js b/packages/core/admin/admin/src/components/LocalesProvider/context.js deleted file mode 100644 index 1621c38aa6..0000000000 --- a/packages/core/admin/admin/src/components/LocalesProvider/context.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react'; - -const LocalesProviderContext = createContext(); - -export default LocalesProviderContext; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/index.js b/packages/core/admin/admin/src/components/LocalesProvider/index.js deleted file mode 100644 index 32a7fa0e91..0000000000 --- a/packages/core/admin/admin/src/components/LocalesProvider/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import PropTypes from 'prop-types'; - -import LocalesProviderContext from './context'; - -const LocalesProvider = ({ changeLocale, children, localeNames }) => { - return ( - - {children} - - ); -}; - -LocalesProvider.propTypes = { - changeLocale: PropTypes.func.isRequired, - children: PropTypes.element.isRequired, - localeNames: PropTypes.object.isRequired, -}; - -export default LocalesProvider; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js b/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js deleted file mode 100644 index 7eb2a00001..0000000000 --- a/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import { render } from '@testing-library/react'; - -import LocalesProvider from '../index'; - -describe('LocalesProvider', () => { - it('should not crash', () => { - const { container } = render( - -
Test
-
- ); - - expect(container.firstChild).toMatchInlineSnapshot(` -
- Test -
- `); - }); -}); diff --git a/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js b/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js deleted file mode 100644 index 524bd9b98d..0000000000 --- a/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; - -import LocalesProviderContext from './context'; - -const useLocalesProvider = () => { - const { changeLocale, localeNames, messages } = useContext(LocalesProviderContext); - - return { changeLocale, localeNames, messages }; -}; - -export default useLocalesProvider; diff --git a/packages/core/admin/admin/src/components/Providers/index.js b/packages/core/admin/admin/src/components/Providers/index.js index 8314127d0b..64bf6766bf 100644 --- a/packages/core/admin/admin/src/components/Providers/index.js +++ b/packages/core/admin/admin/src/components/Providers/index.js @@ -12,10 +12,10 @@ import PropTypes from 'prop-types'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; -import { AdminContext } from '../../contexts'; +import { AdminContext } from '../../contexts/admin'; import { ConfigurationProvider } from '../ConfigurationProvider'; import GuidedTour from '../GuidedTour'; -import LanguageProvider from '../LanguageProvider'; +import { LanguageProvider } from '../LanguageProvider'; import { Theme } from '../Theme'; import { ThemeToggleProvider } from '../ThemeToggleProvider'; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/__mocks__/useLocalesProvider.js b/packages/core/admin/admin/src/components/__mocks__/LanguageProvider.js similarity index 68% rename from packages/core/admin/admin/src/components/LocalesProvider/__mocks__/useLocalesProvider.js rename to packages/core/admin/admin/src/components/__mocks__/LanguageProvider.js index d828c887fe..bf33e4e262 100644 --- a/packages/core/admin/admin/src/components/LocalesProvider/__mocks__/useLocalesProvider.js +++ b/packages/core/admin/admin/src/components/__mocks__/LanguageProvider.js @@ -1,4 +1,4 @@ -export default function useLocalesProvider() { +export function useLocalesProvider() { return { changeLocale() {}, localeNames: { en: 'English' }, diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js b/packages/core/admin/admin/src/components/tests/LanguageProvider.test.tsx similarity index 58% rename from packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js rename to packages/core/admin/admin/src/components/tests/LanguageProvider.test.tsx index de659b72f0..78dcd5a1fe 100644 --- a/packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js +++ b/packages/core/admin/admin/src/components/tests/LanguageProvider.test.tsx @@ -1,43 +1,39 @@ -import React from 'react'; - -import { fireEvent, render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useIntl } from 'react-intl'; -import en from '../../../translations/en.json'; -import fr from '../../../translations/fr.json'; -import useLocalesProvider from '../../LocalesProvider/useLocalesProvider'; -import LanguageProvider from '../index'; +import en from '../../translations/en.json'; +import fr from '../../translations/fr.json'; +import { LanguageProvider, useLocales } from '../LanguageProvider'; const messages = { en, fr }; const localeNames = { en: 'English', fr: 'Français' }; +const user = userEvent.setup(); + describe('LanguageProvider', () => { afterEach(() => { localStorage.removeItem('strapi-admin-language'); }); it('should not crash', () => { - const { container } = render( + const { getByText } = render(
Test
); - expect(container.firstChild).toMatchInlineSnapshot(` -
- Test -
- `); + expect(getByText('Test')).toBeInTheDocument(); }); it('should change the locale and set the strapi-admin-language item in the localStorage', async () => { const Test = () => { const { locale } = useIntl(); - const { changeLocale } = useLocalesProvider(); + const { changeLocale } = useLocales(); return (
-

{localeNames[locale]}

+

{localeNames[locale as keyof typeof messages]}

@@ -45,7 +41,7 @@ describe('LanguageProvider', () => { ); }; - render( + const { getByText, getByRole } = render( @@ -53,11 +49,11 @@ describe('LanguageProvider', () => { expect(localStorage.getItem('strapi-admin-language')).toEqual('en'); - expect(screen.getByText('English')).toBeInTheDocument(); + expect(getByText('English')).toBeInTheDocument(); - fireEvent.click(screen.getByText('CHANGE')); + await user.click(getByRole('button', { name: 'CHANGE' })); - expect(screen.getByText('Français')).toBeInTheDocument(); + expect(getByText('Français')).toBeInTheDocument(); expect(localStorage.getItem('strapi-admin-language')).toEqual('fr'); }); }); diff --git a/packages/core/admin/admin/src/contexts/Admin/index.js b/packages/core/admin/admin/src/contexts/Admin/index.js deleted file mode 100644 index 4ae3530cf0..0000000000 --- a/packages/core/admin/admin/src/contexts/Admin/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react'; - -const AdminContext = createContext({}); - -export default AdminContext; diff --git a/packages/core/admin/admin/src/contexts/MarketPlace/index.js b/packages/core/admin/admin/src/contexts/MarketPlace/index.js deleted file mode 100644 index 30b2b9370b..0000000000 --- a/packages/core/admin/admin/src/contexts/MarketPlace/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { createContext, useContext } from 'react'; - -import PropTypes from 'prop-types'; - -const MarketPlaceContext = createContext({}); - -const MarketPlaceContextProvider = ({ children, ...rest }) => { - return {children}; -}; - -const useMarketPlaceContext = () => useContext(MarketPlaceContext); - -MarketPlaceContextProvider.propTypes = { - children: PropTypes.node.isRequired, - downloadPlugin: PropTypes.func.isRequired, -}; - -export { MarketPlaceContext, MarketPlaceContextProvider, useMarketPlaceContext }; diff --git a/packages/core/admin/admin/src/contexts/admin.ts b/packages/core/admin/admin/src/contexts/admin.ts new file mode 100644 index 0000000000..33667d2d65 --- /dev/null +++ b/packages/core/admin/admin/src/contexts/admin.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; + +interface AdminContextValue { + /** + * TODO: this should come from `StrapiApp['getAdminInjectedComponents']` + */ + getAdminInjectedComponents: () => unknown; +} + +const AdminContext = React.createContext({ + getAdminInjectedComponents() { + throw new Error('AdminContext: getAdminInjectedComponents() not implemented'); + }, +}); + +const useAdmin = () => React.useContext(AdminContext); + +export { AdminContext, useAdmin }; diff --git a/packages/core/admin/admin/src/contexts/index.js b/packages/core/admin/admin/src/contexts/index.js deleted file mode 100644 index b3e2abe3c2..0000000000 --- a/packages/core/admin/admin/src/contexts/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as AdminContext } from './Admin'; diff --git a/packages/core/admin/admin/src/hooks/index.js b/packages/core/admin/admin/src/hooks/index.js index 50e01ba30f..0352ee32d5 100644 --- a/packages/core/admin/admin/src/hooks/index.js +++ b/packages/core/admin/admin/src/hooks/index.js @@ -1,5 +1,3 @@ -export { useConfiguration } from './useConfiguration'; export { useContentTypes } from './useContentTypes'; -export { default as useMenu } from './useMenu'; export { default as useSettingsForm } from './useSettingsForm'; export { default as useSettingsMenu } from './useSettingsMenu'; diff --git a/packages/core/admin/admin/src/hooks/useMenu.ts b/packages/core/admin/admin/src/hooks/useMenu.ts new file mode 100644 index 0000000000..5f87faca9e --- /dev/null +++ b/packages/core/admin/admin/src/hooks/useMenu.ts @@ -0,0 +1,153 @@ +import * as React from 'react'; + +import { + Permission, + hasPermissions, + useAppInfo, + useRBACProvider, + useStrapiApp, + StrapiAppContextValue, +} from '@strapi/helper-plugin'; +import { Cog, Puzzle, ShoppingCart } from '@strapi/icons'; +import cloneDeep from 'lodash/cloneDeep'; +import { useSelector } from 'react-redux'; + +// @ts-expect-error - no types, yet. +import { selectAdminPermissions } from '../pages/App/selectors'; + +/* ------------------------------------------------------------------------------------------------- + * useMenu + * -----------------------------------------------------------------------------------------------*/ + +type MenuItem = StrapiAppContextValue['menu'][number]; + +export interface Menu { + generalSectionLinks: MenuItem[]; + pluginsSectionLinks: MenuItem[]; + isLoading: boolean; +} + +const useMenu = () => { + const { allPermissions: userPermissions } = useRBACProvider(); + const { shouldUpdateStrapi } = useAppInfo(); + const { menu } = useStrapiApp(); + const permissions = useSelector(selectAdminPermissions); + const [menuWithUserPermissions, setMenuWithUserPermissions] = React.useState({ + generalSectionLinks: [ + { + icon: Puzzle, + intlLabel: { + id: 'global.plugins', + defaultMessage: 'Plugins', + }, + to: '/list-plugins', + // @ts-expect-error - we need the permissions type from the plugin + permissions: permissions.marketplace.main, + }, + { + icon: ShoppingCart, + intlLabel: { + id: 'global.marketplace', + defaultMessage: 'Marketplace', + }, + to: '/marketplace', + // @ts-expect-error - we need the permissions type from the plugin + permissions: permissions.marketplace.main, + }, + { + icon: Cog, + intlLabel: { + id: 'global.settings', + defaultMessage: 'Settings', + }, + to: '/settings', + // Permissions of this link are retrieved in the init phase + // using the settings menu + permissions: [], + notificationsCount: 0, + }, + ], + pluginsSectionLinks: [], + isLoading: true, + }); + const generalSectionLinksRef = React.useRef(menuWithUserPermissions.generalSectionLinks); + + React.useEffect(() => { + async function applyMenuPermissions() { + const authorizedPluginSectionLinks = await getPluginSectionLinks(userPermissions, menu); + + const authorizedGeneralSectionLinks = await getGeneralLinks( + userPermissions, + generalSectionLinksRef.current, + shouldUpdateStrapi + ); + + setMenuWithUserPermissions((state) => ({ + ...state, + generalSectionLinks: authorizedGeneralSectionLinks, + pluginsSectionLinks: authorizedPluginSectionLinks, + isLoading: false, + })); + } + + applyMenuPermissions(); + }, [ + setMenuWithUserPermissions, + generalSectionLinksRef, + userPermissions, + menu, + permissions, + shouldUpdateStrapi, + ]); + + return menuWithUserPermissions; +}; + +/* ------------------------------------------------------------------------------------------------- + * Utils + * -----------------------------------------------------------------------------------------------*/ + +const getGeneralLinks = async ( + userPermissions: Permission[], + generalSectionRawLinks: MenuItem[], + shouldUpdateStrapi: boolean = false +) => { + const generalSectionLinksPermissions = await Promise.all( + generalSectionRawLinks.map(({ permissions }) => hasPermissions(userPermissions, permissions)) + ); + + const authorizedGeneralSectionLinks = generalSectionRawLinks.filter( + (_, index) => generalSectionLinksPermissions[index] + ); + + const settingsLinkIndex = authorizedGeneralSectionLinks.findIndex( + (obj) => obj.to === '/settings' + ); + + if (settingsLinkIndex === -1) { + return []; + } + + const authorizedGeneralLinksClone = cloneDeep(authorizedGeneralSectionLinks); + + authorizedGeneralLinksClone[settingsLinkIndex].notificationsCount = shouldUpdateStrapi ? 1 : 0; + + return authorizedGeneralLinksClone; +}; + +const getPluginSectionLinks = async ( + userPermissions: Permission[], + pluginsSectionRawLinks: MenuItem[] +) => { + const pluginSectionLinksPermissions = await Promise.all( + pluginsSectionRawLinks.map(({ permissions }) => hasPermissions(userPermissions, permissions)) + ); + + const authorizedPluginSectionLinks = pluginsSectionRawLinks.filter( + (_, index) => pluginSectionLinksPermissions[index] + ); + + return authorizedPluginSectionLinks; +}; + +export { useMenu }; diff --git a/packages/core/admin/admin/src/hooks/useMenu/index.js b/packages/core/admin/admin/src/hooks/useMenu/index.js deleted file mode 100644 index 34d17666f3..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/index.js +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from 'react'; - -import { useAppInfo, useRBACProvider, useStrapiApp } from '@strapi/helper-plugin'; -import { Cog, Puzzle, ShoppingCart } from '@strapi/icons'; -import { useSelector } from 'react-redux'; - -import { selectAdminPermissions } from '../../pages/App/selectors'; - -import getGeneralLinks from './utils/getGeneralLinks'; -import getPluginSectionLinks from './utils/getPluginSectionLinks'; - -const useMenu = () => { - const { allPermissions: userPermissions } = useRBACProvider(); - const { shouldUpdateStrapi } = useAppInfo(); - const { menu } = useStrapiApp(); - const permissions = useSelector(selectAdminPermissions); - const [menuWithUserPermissions, setMenuWithUserPermissions] = React.useState({ - generalSectionLinks: [ - { - icon: Puzzle, - intlLabel: { - id: 'global.plugins', - defaultMessage: 'Plugins', - }, - to: '/list-plugins', - permissions: permissions.marketplace.main, - }, - { - icon: ShoppingCart, - intlLabel: { - id: 'global.marketplace', - defaultMessage: 'Marketplace', - }, - to: '/marketplace', - permissions: permissions.marketplace.main, - }, - { - icon: Cog, - intlLabel: { - id: 'global.settings', - defaultMessage: 'Settings', - }, - to: '/settings', - // Permissions of this link are retrieved in the init phase - // using the settings menu - permissions: [], - notificationsCount: 0, - }, - ], - pluginsSectionLinks: [], - isLoading: true, - }); - const generalSectionLinksRef = React.useRef(menuWithUserPermissions.generalSectionLinks); - - React.useEffect(() => { - async function applyMenuPermissions() { - const authorizedPluginSectionLinks = await getPluginSectionLinks(userPermissions, menu); - - const authorizedGeneralSectionLinks = await getGeneralLinks( - userPermissions, - generalSectionLinksRef.current, - shouldUpdateStrapi - ); - - setMenuWithUserPermissions((state) => ({ - ...state, - generalSectionLinks: authorizedGeneralSectionLinks, - pluginsSectionLinks: authorizedPluginSectionLinks, - isLoading: false, - })); - } - - applyMenuPermissions(); - }, [ - setMenuWithUserPermissions, - generalSectionLinksRef, - userPermissions, - menu, - permissions, - shouldUpdateStrapi, - ]); - - return menuWithUserPermissions; -}; - -export default useMenu; diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/checkPermissions.js b/packages/core/admin/admin/src/hooks/useMenu/utils/checkPermissions.js deleted file mode 100644 index 94b061a97e..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/checkPermissions.js +++ /dev/null @@ -1,13 +0,0 @@ -import { hasPermissions } from '@strapi/helper-plugin'; - -/** - * This function resolves an array of Promises - * It puts at a specific index the status of a specific permission. - * While this might look weird, we then iterate on this array - * and check the different CT/ST/general/plugin sections - * and make an index based comparisons - */ -const checkPermissions = (userPermissions, permissionsToCheck) => - permissionsToCheck.map(({ permissions }) => hasPermissions(userPermissions, permissions)); - -export default checkPermissions; diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/getGeneralLinks.js b/packages/core/admin/admin/src/hooks/useMenu/utils/getGeneralLinks.js deleted file mode 100644 index fac6937a1f..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/getGeneralLinks.js +++ /dev/null @@ -1,31 +0,0 @@ -import cloneDeep from 'lodash/cloneDeep'; - -import checkPermissions from './checkPermissions'; - -const getGeneralLinks = async (userPermissions, generalSectionRawLinks, shouldUpdateStrapi) => { - const generalSectionPermissionsPromises = checkPermissions( - userPermissions, - generalSectionRawLinks - ); - const generalSectionLinksPermissions = await Promise.all(generalSectionPermissionsPromises); - - const authorizedGeneralSectionLinks = generalSectionRawLinks.filter( - (_, index) => generalSectionLinksPermissions[index] - ); - - const settingsLinkIndex = authorizedGeneralSectionLinks.findIndex( - (obj) => obj.to === '/settings' - ); - - if (settingsLinkIndex === -1) { - return []; - } - - const authorizedGeneralLinksClone = cloneDeep(authorizedGeneralSectionLinks); - - authorizedGeneralLinksClone[settingsLinkIndex].notificationsCount = shouldUpdateStrapi ? 1 : 0; - - return authorizedGeneralLinksClone; -}; - -export default getGeneralLinks; diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/getPluginSectionLinks.js b/packages/core/admin/admin/src/hooks/useMenu/utils/getPluginSectionLinks.js deleted file mode 100644 index 13a60da232..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/getPluginSectionLinks.js +++ /dev/null @@ -1,17 +0,0 @@ -import checkPermissions from './checkPermissions'; - -const getPluginSectionLinks = async (userPermissions, pluginsSectionRawLinks) => { - const pluginSectionPermissionsPromises = checkPermissions( - userPermissions, - pluginsSectionRawLinks - ); - const pluginSectionLinksPermissions = await Promise.all(pluginSectionPermissionsPromises); - - const authorizedPluginSectionLinks = pluginsSectionRawLinks.filter( - (_, index) => pluginSectionLinksPermissions[index] - ); - - return authorizedPluginSectionLinks; -}; - -export default getPluginSectionLinks; diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/checkPermissions.test.js b/packages/core/admin/admin/src/hooks/useMenu/utils/tests/checkPermissions.test.js deleted file mode 100644 index 64b2a3eec7..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/checkPermissions.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import checkPermissions from '../checkPermissions'; - -jest.mock('@strapi/helper-plugin', () => ({ - hasPermissions: () => Promise.resolve(true), -})); - -describe('checkPermissions', () => { - it('creates an array of boolean corresponding to the permission state', async () => { - const userPermissions = {}; - const permissions = [{ permissions: {} }, { permissions: {} }]; - - const expected = [true, true]; - const actual = await Promise.all(checkPermissions(userPermissions, permissions)); - - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getGeneralLinks.test.js b/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getGeneralLinks.test.js deleted file mode 100644 index 1f58125c5b..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getGeneralLinks.test.js +++ /dev/null @@ -1,223 +0,0 @@ -import { hasPermissions } from '@strapi/helper-plugin'; - -import getGeneralLinks from '../getGeneralLinks'; - -jest.mock('@strapi/helper-plugin', () => ({ - ...jest.requireActual('@strapi/helper-plugin'), - hasPermissions: jest.fn().mockResolvedValue(true), -})); - -describe('getGeneralLinks', () => { - it('resolves valid general links from real data', async () => { - const permissions = [ - { - id: 458, - action: 'plugin::i18n.locale.read', - subject: null, - properties: {}, - conditions: [], - }, - { - id: 459, - action: 'plugin::content-manager.explorer.create', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 460, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 461, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - { - id: 462, - action: 'plugin::content-manager.explorer.update', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - ]; - const generalSectionRawLinks = [ - { - icon: 'list', - label: 'app.components.LeftMenuLinkContainer.listPlugins', - to: '/list-plugins', - isDisplayed: false, - permissions: [ - { - action: 'admin::marketplace.read', - subject: null, - }, - ], - notificationsCount: 0, - }, - { - icon: 'shopping-basket', - label: 'app.components.LeftMenuLinkContainer.installNewPlugin', - to: '/marketplace', - isDisplayed: false, - permissions: [ - { - action: 'admin::marketplace.read', - subject: null, - }, - ], - notificationsCount: 0, - }, - { - icon: 'cog', - label: 'app.components.LeftMenuLinkContainer.settings', - isDisplayed: true, - to: '/settings', - permissions: [], - notificationsCount: 0, - }, - ]; - - const expected = [ - { - icon: 'list', - label: 'app.components.LeftMenuLinkContainer.listPlugins', - to: '/list-plugins', - isDisplayed: false, - permissions: [ - { - action: 'admin::marketplace.read', - subject: null, - }, - ], - notificationsCount: 0, - }, - { - icon: 'shopping-basket', - label: 'app.components.LeftMenuLinkContainer.installNewPlugin', - to: '/marketplace', - isDisplayed: false, - permissions: [ - { - action: 'admin::marketplace.read', - subject: null, - }, - ], - notificationsCount: 0, - }, - { - icon: 'cog', - label: 'app.components.LeftMenuLinkContainer.settings', - isDisplayed: true, - to: '/settings', - permissions: [], - notificationsCount: 0, - }, - ]; - const actual = await getGeneralLinks(permissions, generalSectionRawLinks, false); - - expect(actual).toEqual(expected); - }); - - it('resolves an empty array when the to (/settings) is not in the authorized links', async () => { - hasPermissions.mockImplementation(() => Promise.resolve(false)); - - const permissions = [ - { - id: 458, - action: 'plugin::i18n.locale.read', - subject: null, - properties: {}, - conditions: [], - }, - { - id: 459, - action: 'plugin::content-manager.explorer.create', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 460, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 461, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - { - id: 462, - action: 'plugin::content-manager.explorer.update', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - ]; - const generalSectionRawLinks = [ - { - icon: 'list', - label: 'app.components.LeftMenuLinkContainer.listPlugins', - to: '/list-plugins', - isDisplayed: false, - permissions: [], - notificationsCount: 0, - }, - { - icon: 'shopping-basket', - label: 'app.components.LeftMenuLinkContainer.installNewPlugin', - to: '/marketplace', - isDisplayed: false, - permissions: [], - notificationsCount: 0, - }, - { - icon: 'cog', - label: 'app.components.LeftMenuLinkContainer.settings', - isDisplayed: false, - to: '/settings', - permissions: [], - notificationsCount: 0, - }, - ]; - - const expected = []; - const actual = await getGeneralLinks(permissions, generalSectionRawLinks, false); - - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getPluginSectionLinks.test.js b/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getPluginSectionLinks.test.js deleted file mode 100644 index 2eb99ac26a..0000000000 --- a/packages/core/admin/admin/src/hooks/useMenu/utils/tests/getPluginSectionLinks.test.js +++ /dev/null @@ -1,129 +0,0 @@ -import getPluginSectionLinks from '../getPluginSectionLinks'; - -jest.mock('@strapi/helper-plugin', () => ({ - ...jest.requireActual('@strapi/helper-plugin'), - hasPermissions: jest.fn().mockResolvedValue(true), -})); - -describe('getPluginSectionLinks', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('creates an array of boolean corresponding to the permission state', async () => { - const userPermissions = [ - { - id: 458, - action: 'plugin::i18n.locale.read', - subject: null, - properties: {}, - conditions: [], - }, - { - id: 459, - action: 'plugin::content-manager.explorer.create', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 460, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['en'], - }, - conditions: [], - }, - { - id: 461, - action: 'plugin::content-manager.explorer.read', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - { - id: 462, - action: 'plugin::content-manager.explorer.update', - subject: 'api::article.article', - properties: { - fields: ['Name'], - locales: ['fr-FR'], - }, - conditions: [], - }, - ]; - - const pluginsSectionRawLinks = [ - { - destination: '/plugins/content-type-builder', - icon: 'paint-brush', - label: { - id: 'content-type-builder.plugin.name', - defaultMessage: 'Content-Types Builder', - }, - permissions: [ - { - action: 'plugin::content-type-builder.read', - subject: null, - }, - ], - isDisplayed: false, - }, - { - destination: '/plugins/upload', - icon: 'cloud-upload-alt', - label: { - id: 'upload.plugin.name', - defaultMessage: 'Media Library', - }, - permissions: [ - { - action: 'plugin::upload.read', - subject: null, - }, - { - action: 'plugin::upload.assets.create', - subject: null, - }, - { - action: 'plugin::upload.assets.update', - subject: null, - }, - ], - isDisplayed: false, - }, - ]; - - const expected = [ - { - destination: '/plugins/content-type-builder', - icon: 'paint-brush', - isDisplayed: false, - label: { defaultMessage: 'Content-Types Builder', id: 'content-type-builder.plugin.name' }, - permissions: [{ action: 'plugin::content-type-builder.read', subject: null }], - }, - { - destination: '/plugins/upload', - icon: 'cloud-upload-alt', - isDisplayed: false, - label: { defaultMessage: 'Media Library', id: 'upload.plugin.name' }, - permissions: [ - { action: 'plugin::upload.read', subject: null }, - { action: 'plugin::upload.assets.create', subject: null }, - { action: 'plugin::upload.assets.update', subject: null }, - ], - }, - ]; - const actual = await getPluginSectionLinks(userPermissions, pluginsSectionRawLinks); - - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/core/admin/admin/src/layouts/UnauthenticatedLayout/LocaleToggle/index.js b/packages/core/admin/admin/src/layouts/UnauthenticatedLayout/LocaleToggle/index.js index 800f3e7445..780f32adc5 100644 --- a/packages/core/admin/admin/src/layouts/UnauthenticatedLayout/LocaleToggle/index.js +++ b/packages/core/admin/admin/src/layouts/UnauthenticatedLayout/LocaleToggle/index.js @@ -9,10 +9,10 @@ import React from 'react'; import { SingleSelect, SingleSelectOption } from '@strapi/design-system'; import { useIntl } from 'react-intl'; -import useLocalesProvider from '../../../components/LocalesProvider/useLocalesProvider'; +import { useLocales } from '../../../components/LanguageProvider'; const LocaleToggle = () => { - const { changeLocale, localeNames } = useLocalesProvider(); + const { changeLocale, localeNames } = useLocales(); const { locale } = useIntl(); return ( diff --git a/packages/core/admin/admin/src/pages/Admin/index.js b/packages/core/admin/admin/src/pages/Admin/index.js index 8f0c4cb933..13bb46b8af 100644 --- a/packages/core/admin/admin/src/pages/Admin/index.js +++ b/packages/core/admin/admin/src/pages/Admin/index.js @@ -13,8 +13,9 @@ import { useDispatch, useSelector } from 'react-redux'; import { Route, Switch } from 'react-router-dom'; import GuidedTourModal from '../../components/GuidedTour/Modal'; -import LeftMenu from '../../components/LeftMenu'; -import { useConfiguration, useMenu } from '../../hooks'; +import { LeftMenu } from '../../components/LeftMenu'; +import { useConfiguration } from '../../hooks/useConfiguration'; +import { useMenu } from '../../hooks/useMenu'; import AppLayout from '../../layouts/AppLayout'; import { createRoute } from '../../utils'; import { SET_APP_RUNTIME_STATUS } from '../App/constants'; diff --git a/packages/core/admin/admin/src/pages/Admin/tests/index.test.js b/packages/core/admin/admin/src/pages/Admin/tests/index.test.js index 14853a6bb2..1e81f967cd 100644 --- a/packages/core/admin/admin/src/pages/Admin/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/Admin/tests/index.test.js @@ -7,7 +7,7 @@ import { createMemoryHistory } from 'history'; import { IntlProvider } from 'react-intl'; import { Router } from 'react-router-dom'; -import { useMenu } from '../../../hooks'; +import { useMenu } from '../../../hooks/useMenu'; import Admin from '../index'; jest.mock('react-redux', () => ({ @@ -34,14 +34,15 @@ jest.mock('@strapi/helper-plugin', () => ({ })), })); -jest.mock('../../../hooks', () => ({ +jest.mock('../../../hooks/useMenu', () => ({ useMenu: jest.fn(() => ({ isLoading: true, generalSectionLinks: [], pluginsSectionLinks: [] })), - useConfiguration: jest.fn(() => ({ showTutorials: false })), })); -jest.mock('../../../components/LeftMenu', () => () => { - return
menu
; -}); +jest.mock('../../../components/LeftMenu', () => ({ + LeftMenu() { + return
menu
; + }, +})); jest.mock('../../HomePage', () => () => { return
HomePage
; }); diff --git a/packages/core/admin/admin/src/pages/App/index.js b/packages/core/admin/admin/src/pages/App/index.js index 672a69d691..8009c739d8 100644 --- a/packages/core/admin/admin/src/pages/App/index.js +++ b/packages/core/admin/admin/src/pages/App/index.js @@ -23,7 +23,7 @@ import { Route, Switch } from 'react-router-dom'; import { PrivateRoute } from '../../components/PrivateRoute'; import { ADMIN_PERMISSIONS_CE } from '../../constants'; -import { useConfiguration } from '../../hooks'; +import { useConfiguration } from '../../hooks/useConfiguration'; import { useEnterprise } from '../../hooks/useEnterprise'; import { createRoute, makeUniqueRoutes } from '../../utils'; import AuthPage from '../AuthPage'; diff --git a/packages/core/admin/admin/src/pages/AuthPage/components/Oops/tests/index.test.js b/packages/core/admin/admin/src/pages/AuthPage/components/Oops/tests/index.test.js index 0400d47be1..0dba2d7a30 100644 --- a/packages/core/admin/admin/src/pages/AuthPage/components/Oops/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/AuthPage/components/Oops/tests/index.test.js @@ -8,18 +8,6 @@ import { Router } from 'react-router-dom'; import Oops from '..'; -jest.mock('../../../../../components/LocalesProvider/useLocalesProvider', () => () => ({ - changeLocale() {}, - localeNames: { en: 'English' }, - messages: ['test'], -})); -jest.mock('../../../../../hooks/useConfiguration', () => ({ - useConfiguration: () => ({ - logos: { - auth: { custom: 'customAuthLogo.png', default: 'defaultAuthLogo.png' }, - }, - }), -})); describe('ADMIN | PAGES | AUTH | Oops', () => { it('should render and match the snapshot', () => { const history = createMemoryHistory(); @@ -34,7 +22,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { ); expect(container.firstChild).toMatchInlineSnapshot(` - .c9 { + .c8 { font-size: 0.875rem; line-height: 1.43; display: block; @@ -44,20 +32,20 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { color: #32324d; } - .c21 { + .c20 { font-weight: 600; font-size: 2rem; line-height: 1.25; color: #32324d; } - .c22 { + .c21 { font-size: 0.875rem; line-height: 1.43; color: #32324d; } - .c26 { + .c25 { font-size: 0.875rem; line-height: 1.43; color: #4945ff; @@ -85,12 +73,12 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { flex: 1; } - .c13 { + .c12 { padding-top: 8px; padding-bottom: 64px; } - .c15 { + .c14 { background: #ffffff; padding-top: 48px; padding-right: 56px; @@ -100,12 +88,12 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { box-shadow: 0px 1px 4px rgba(33,33,52,0.1); } - .c20 { + .c19 { padding-top: 24px; padding-bottom: 32px; } - .c23 { + .c22 { padding-top: 16px; } @@ -176,7 +164,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { gap: 12px; } - .c17 { + .c16 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -190,7 +178,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { flex-direction: row; } - .c24 { + .c23 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -232,21 +220,21 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { box-shadow: #4945ff 0px 0px 0px 2px; } - .c12 > svg { + .c11 > svg { width: 0.375rem; } - .c12 > svg > path { + .c11 > svg > path { fill: #666687; } - .c10 { + .c9 { -webkit-flex: 1; -ms-flex: 1; flex: 1; } - .c11 { + .c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -257,16 +245,11 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { flex-wrap: wrap; } - .c27[data-state='checked'] .c8 { - font-weight: bold; - color: #4945ff; - } - - .c14:focus-visible { + .c13:focus-visible { outline: none; } - .c25 { + .c24 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -282,23 +265,23 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { outline: none; } - .c25 svg { + .c24 svg { font-size: 0.625rem; } - .c25 svg path { + .c24 svg path { fill: #4945ff; } - .c25:hover { + .c24:hover { color: #7b79ff; } - .c25:active { + .c24:active { color: #271fe0; } - .c25:after { + .c24:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -313,11 +296,11 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { border: 2px solid transparent; } - .c25:focus-visible { + .c24:focus-visible { outline: none; } - .c25:focus-visible:after { + .c24:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -328,16 +311,16 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { border: 2px solid #4945ff; } - .c19 { + .c18 { height: 4.5rem; } - .c16 { + .c15 { margin: 0 auto; width: 552px; } - .c18 { + .c17 { -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -373,13 +356,11 @@ describe('ADMIN | PAGES | AUTH | Oops', () => { class="c6 c7" > - English - + class="c10" + /> { >

Oops...

Your account has been suspended.
If this is a mistake, please contact your administrator. @@ -457,17 +438,17 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
Sign in diff --git a/packages/core/admin/admin/src/pages/AuthPage/components/Register/tests/index.test.js b/packages/core/admin/admin/src/pages/AuthPage/components/Register/tests/index.test.js index fd0bd18121..595c8db467 100644 --- a/packages/core/admin/admin/src/pages/AuthPage/components/Register/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/AuthPage/components/Register/tests/index.test.js @@ -16,8 +16,6 @@ jest.mock('../../../../../hooks/useConfiguration', () => ({ }), })); -jest.mock('../../../../../components/LocalesProvider/useLocalesProvider'); - describe('AUTH | Register', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core/admin/admin/src/pages/AuthPage/components/ResetPassword/tests/index.test.js b/packages/core/admin/admin/src/pages/AuthPage/components/ResetPassword/tests/index.test.js index 838133169b..1b6f2923cb 100644 --- a/packages/core/admin/admin/src/pages/AuthPage/components/ResetPassword/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/AuthPage/components/ResetPassword/tests/index.test.js @@ -9,18 +9,6 @@ import * as yup from 'yup'; import ResetPassword from '..'; -jest.mock('../../../../../components/LocalesProvider/useLocalesProvider', () => () => ({ - changeLocale() {}, - localeNames: { en: 'English' }, - messages: ['test'], -})); -jest.mock('../../../../../hooks/useConfiguration', () => ({ - useConfiguration: () => ({ - logos: { - auth: { custom: 'customAuthLogo.png', default: 'defaultAuthLogo.png' }, - }, - }), -})); jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useNotification: () => jest.fn({}), @@ -498,11 +486,6 @@ describe('ADMIN | PAGES | AUTH | ResetPassword', () => { flex-wrap: wrap; } - .c47[data-state='checked'] .c8 { - font-weight: bold; - color: #4945ff; - } - .c29 { border: none; border-radius: 4px; @@ -697,9 +680,7 @@ describe('ADMIN | PAGES | AUTH | ResetPassword', () => { > - English - + /> { alt="" aria-hidden="true" class="c19" - src="customAuthLogo.png" + src="" />
{ push, location: { search }, } = useHistory(); - const { changeLocale } = useLocalesProvider(); + const { changeLocale } = useLocales(); const { setSkipped } = useGuidedTour(); const { trackUsage } = useTracking(); const { diff --git a/packages/core/admin/admin/src/pages/ProfilePage/index.js b/packages/core/admin/admin/src/pages/ProfilePage/index.js index e3774130e3..f961c45674 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage/index.js +++ b/packages/core/admin/admin/src/pages/ProfilePage/index.js @@ -26,7 +26,7 @@ import { Helmet } from 'react-helmet'; import { useIntl } from 'react-intl'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import useLocalesProvider from '../../components/LocalesProvider/useLocalesProvider'; +import { useLocales } from '../../components/LanguageProvider'; import { useThemeToggle } from '../../hooks/useThemeToggle'; import { getFullName } from '../../utils'; @@ -36,7 +36,7 @@ import UserInfo from './components/UserInfo'; import schema from './utils/schema'; const ProfilePage = () => { - const { changeLocale, localeNames } = useLocalesProvider(); + const { changeLocale, localeNames } = useLocales(); const { setUserDisplayName } = useAppInfo(); const queryClient = useQueryClient(); const { formatMessage } = useIntl(); diff --git a/packages/core/admin/admin/src/pages/ProfilePage/tests/index.test.js b/packages/core/admin/admin/src/pages/ProfilePage/tests/index.test.js index 4edbaef8fc..f3a2dd15a2 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/ProfilePage/tests/index.test.js @@ -5,8 +5,6 @@ import { rest } from 'msw'; import ProfilePage from '../index'; -jest.mock('../../../components/LocalesProvider/useLocalesProvider'); - jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useFocusWhenNavigate: jest.fn(), diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/index.js index c1c71c6b8f..7098544f97 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/index.js @@ -5,7 +5,7 @@ import { useTracking } from '@strapi/helper-plugin'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; -import { useConfiguration } from '../../../../../../hooks'; +import { useConfiguration } from '../../../../../../hooks/useConfiguration'; import { DIMENSION, SIZE } from '../../utils/constants'; import LogoInput from '../LogoInput'; diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/tests/index.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/tests/index.test.js index d966b529b4..ba66aab444 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/CustomizationInfos/tests/index.test.js @@ -25,7 +25,7 @@ const PROJECT_SETTINGS_DATA_FIXTURES = { }, }; -jest.mock('../../../../../../../hooks', () => ({ +jest.mock('../../../../../../../hooks/useConfiguration', () => ({ useConfiguration: jest.fn(() => ({ logos: { menu: { custom: 'customMenuLogo.png', default: 'defaultMenuLogo.png' }, diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/index.js index 75953b64b7..9eccdc4900 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/index.js @@ -29,7 +29,7 @@ import { useIntl } from 'react-intl'; import { useMutation, useQuery } from 'react-query'; import { useSelector } from 'react-redux'; -import { useConfiguration } from '../../../../hooks'; +import { useConfiguration } from '../../../../hooks/useConfiguration'; import { useEnterprise } from '../../../../hooks/useEnterprise'; import { selectAdminPermissions } from '../../../App/selectors'; diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/tests/index.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/tests/index.test.js index 85b84a90c3..ec8221f1df 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/tests/index.test.js @@ -16,7 +16,7 @@ jest.mock('@strapi/helper-plugin', () => ({ useRBAC: jest.fn(() => ({ allowedActions: { canRead: true, canUpdate: true } })), })); -jest.mock('../../../../../hooks', () => ({ +jest.mock('../../../../../hooks/useConfiguration', () => ({ useConfiguration: jest.fn(() => ({ logos: { menu: { custom: 'customMenuLogo.png', default: 'defaultMenuLogo.png' }, diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/tests/index.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/tests/index.test.js index 879171dbd4..cd897dcdb2 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/tests/index.test.js @@ -7,7 +7,7 @@ import { createMemoryHistory } from 'history'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Router } from 'react-router-dom'; -import LanguageProvider from '../../../../../../../../components/LanguageProvider'; +import { LanguageProvider } from '../../../../../../../../components/LanguageProvider'; import en from '../../../../../../../../translations/en.json'; import WebhookForm from '../index'; diff --git a/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js b/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js index d887b334b7..1d185722b2 100644 --- a/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js @@ -9,19 +9,6 @@ import { Router } from 'react-router-dom'; import UseCasePage from '../index'; -jest.mock('../../../components/LocalesProvider/useLocalesProvider', () => () => ({ - changeLocale() {}, - localeNames: { en: 'English' }, - messages: ['test'], -})); -jest.mock('../../../hooks/useConfiguration', () => ({ - useConfiguration: () => ({ - logos: { - auth: { custom: 'customAuthLogo.png', default: 'defaultAuthLogo.png' }, - }, - }), -})); - jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useNotification: jest.fn(), @@ -590,9 +577,7 @@ describe('Admin | UseCasePage', () => { > - English - + /> { alt="" aria-hidden="true" class="c19" - src="customAuthLogo.png" + src="" />
{ - const context = useContext(AdminContext); - - return context; -}; - -export default useAdminProvider; diff --git a/packages/core/admin/admin/src/shared/hooks/useInjectionZone/index.js b/packages/core/admin/admin/src/shared/hooks/useInjectionZone/index.js index eca6f0e702..bd3d56bd14 100644 --- a/packages/core/admin/admin/src/shared/hooks/useInjectionZone/index.js +++ b/packages/core/admin/admin/src/shared/hooks/useInjectionZone/index.js @@ -1,7 +1,7 @@ -import useAdminProvider from '../useAdminProvider'; +import { useAdmin } from '../../../contexts/admin'; const useInjectionZone = (area) => { - const { getAdminInjectedComponents } = useAdminProvider(); + const { getAdminInjectedComponents } = useAdmin(); const [moduleName, page, position] = area.split('.'); diff --git a/packages/core/admin/admin/tests/utils.tsx b/packages/core/admin/admin/tests/utils.tsx index 8674a9687e..490af22ff7 100644 --- a/packages/core/admin/admin/tests/utils.tsx +++ b/packages/core/admin/admin/tests/utils.tsx @@ -23,8 +23,7 @@ import { createStore } from 'redux'; // @ts-expect-error – no types yet. import ModelsContext from '../src/content-manager/contexts/ModelsContext'; -// @ts-expect-error – no types yet. -import AdminContext from '../src/contexts/Admin'; +import { AdminContext } from '../src/contexts/admin'; import { ConfigurationContext } from '../src/contexts/configuration'; import { server } from './server'; @@ -63,6 +62,7 @@ const Providers = ({ children, initialEntries }: ProvidersProps) => { ; +export type RBACContextValue = { + allPermissions: Permission[]; // The permissions of the current user. + refetchPermissions: QueryObserverBaseResult['refetch']; }; -const RBACContext: React.Context = React.createContext({}); +const RBACContext = React.createContext({ + allPermissions: [], + refetchPermissions: async () => { + throw new Error('RBACContext: refetchPermissions() not implemented'); + }, +}); /** * @deprecated Use RBACContext instead. diff --git a/packages/core/helper-plugin/src/features/StrapiApp.tsx b/packages/core/helper-plugin/src/features/StrapiApp.tsx index 761dc644d1..b7ac96dd82 100644 --- a/packages/core/helper-plugin/src/features/StrapiApp.tsx +++ b/packages/core/helper-plugin/src/features/StrapiApp.tsx @@ -1,16 +1,23 @@ import * as React from 'react'; +import { LinkProps } from 'react-router-dom'; + import { TranslationMessage } from '../types'; import type { domain } from '@strapi/permissions'; type Permission = domain.permission.Permission; -interface MenuItem { +interface MenuItem extends Pick { to: string; - icon: React.ComponentType; + icon: React.ElementType; intlLabel: TranslationMessage; - permissions?: Permission[]; + /** + * TODO: add type from the BE for what an Admin Permission looks like – + * most likely shared throught the helper plugin...? or duplicated, idm. + */ + permissions: Permission[]; + notificationsCount?: number; Component?: React.ComponentType; } diff --git a/packages/core/helper-plugin/tests/utils.tsx b/packages/core/helper-plugin/tests/utils.tsx index 16ee6a4448..7ea6c2522b 100644 --- a/packages/core/helper-plugin/tests/utils.tsx +++ b/packages/core/helper-plugin/tests/utils.tsx @@ -16,7 +16,7 @@ import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from 'react-query'; import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; -import { RBACContext } from '../src/features/RBAC'; +import { RBACContext, RBACContextValue } from '../src/features/RBAC'; import { server } from './server'; @@ -26,9 +26,10 @@ interface ProvidersProps { } const Providers = ({ children, initialEntries }: ProvidersProps) => { - const rbacContextValue = React.useMemo( + const rbacContextValue: RBACContextValue = React.useMemo( () => ({ allPermissions: fixtures.permissions.allPermissions, + refetchPermissions: jest.fn(), }), [] ); diff --git a/packages/core/helper-plugin/tsconfig.build.json b/packages/core/helper-plugin/tsconfig.build.json index 3a1e5ce1d2..6c29561154 100644 --- a/packages/core/helper-plugin/tsconfig.build.json +++ b/packages/core/helper-plugin/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "exclude": ["**/*.test.tsx", "**/*.test.ts"], "compilerOptions": { + "declarationMap": true, "outDir": "./dist" } }