chore(admin): convert more admin components (#18433)

* chore(admin): convert LanguageProvider to ts

* chore(admin): convert more contexts

* chore(helper-plugin): produce declaration maps

* fix(admin): LANGUAGE_LOCAL_STORAGE_KEY export

* chore(admin): convert useMenu

* chore(admin): convert LeftMenu to TS

* test(helper-plugin): fix utils

* chore: fix tests
This commit is contained in:
Josh 2023-10-16 11:05:12 +01:00 committed by GitHub
parent c7b5014253
commit 054cea409d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 447 additions and 973 deletions

View File

@ -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' }}
/>
<BrowserRouter basename={basename}>
<App store={store} />

View File

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

View File

@ -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<string, string>;
}
const LocalesContext = React.createContext<LocalesContextValue>({
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<string, string>;
messages: Record<string, Record<string, string>>;
}
const LanguageProvider = ({ children, localeNames, messages }: LanguageProviderProps) => {
const [{ locale }, dispatch] = React.useReducer<React.Reducer<State, Action>, 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 (
<IntlProvider locale={locale} defaultLocale="en" messages={appMessages} textComponent="span">
<LocalesContext.Provider value={contextValue}>{children}</LocalesContext.Provider>
</IntlProvider>
);
};
/* -------------------------------------------------------------------------------------------------
* Reducer
* -----------------------------------------------------------------------------------------------*/
interface State {
localeNames: Record<string, string>;
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 };

View File

@ -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 (
<IntlProvider locale={locale} defaultLocale="en" messages={appMessages} textComponent="span">
<LocalesProvider changeLocale={changeLocale} localeNames={localeNames}>
{children}
</LocalesProvider>
</IntlProvider>
);
};
LanguageProvider.propTypes = {
children: PropTypes.element.isRequired,
localeNames: PropTypes.objectOf(PropTypes.string).isRequired,
messages: PropTypes.object.isRequired,
};
export default LanguageProvider;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
const localStorageKey = 'strapi-admin-language';
export default localStorageKey;

View File

@ -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<Menu, 'generalSectionLinks' | 'pluginsSectionLinks'> {}
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => {
const navUserRef = React.useRef<HTMLDivElement>(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 }) => {
<NavSections>
<NavLink
as={RouterNavLink}
// @ts-expect-error the props from the passed as prop are not inferred // joined together
to="/content-manager"
icon={<Write />}
onClick={() => handleClickOnLink('/content-manager')}
@ -146,6 +152,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
return (
<NavLink
as={RouterNavLink}
// @ts-expect-error the props from the passed as prop are not inferred // joined together
to={link.to}
key={link.to}
icon={<Icon />}
@ -172,8 +179,11 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
<NavLink
as={RouterNavLink}
badgeContent={
(link.notificationsCount > 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={<LinkIcon />}
@ -190,7 +200,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
<NavFooter>
<NavUser
id="main-nav-user-button"
ref={buttonRef}
ref={navUserRef}
onClick={handleToggleUserLinks}
initials={initials}
>
@ -244,9 +254,4 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
);
};
LeftMenu.propTypes = {
generalSectionLinks: PropTypes.array.isRequired,
pluginsSectionLinks: PropTypes.array.isRequired,
};
export default LeftMenu;
export { LeftMenu };

View File

@ -1,5 +0,0 @@
import { createContext } from 'react';
const LocalesProviderContext = createContext();
export default LocalesProviderContext;

View File

@ -1,21 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import LocalesProviderContext from './context';
const LocalesProvider = ({ changeLocale, children, localeNames }) => {
return (
<LocalesProviderContext.Provider value={{ changeLocale, localeNames }}>
{children}
</LocalesProviderContext.Provider>
);
};
LocalesProvider.propTypes = {
changeLocale: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
localeNames: PropTypes.object.isRequired,
};
export default LocalesProvider;

View File

@ -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(
<LocalesProvider
changeLocale={jest.fn()}
localeNames={{ en: 'English' }}
messages={{ en: {} }}
>
<div>Test</div>
</LocalesProvider>
);
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
Test
</div>
`);
});
});

View File

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

View File

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

View File

@ -1,4 +1,4 @@
export default function useLocalesProvider() {
export function useLocalesProvider() {
return {
changeLocale() {},
localeNames: { en: 'English' },

View File

@ -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(
<LanguageProvider messages={messages} localeNames={localeNames}>
<div>Test</div>
</LanguageProvider>
);
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
Test
</div>
`);
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 (
<div>
<h1>{localeNames[locale]}</h1>
<h1>{localeNames[locale as keyof typeof messages]}</h1>
<button type="button" onClick={() => changeLocale('fr')}>
CHANGE
</button>
@ -45,7 +41,7 @@ describe('LanguageProvider', () => {
);
};
render(
const { getByText, getByRole } = render(
<LanguageProvider messages={messages} localeNames={localeNames}>
<Test />
</LanguageProvider>
@ -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');
});
});

View File

@ -1,5 +0,0 @@
import { createContext } from 'react';
const AdminContext = createContext({});
export default AdminContext;

View File

@ -1,18 +0,0 @@
import React, { createContext, useContext } from 'react';
import PropTypes from 'prop-types';
const MarketPlaceContext = createContext({});
const MarketPlaceContextProvider = ({ children, ...rest }) => {
return <MarketPlaceContext.Provider value={rest}>{children}</MarketPlaceContext.Provider>;
};
const useMarketPlaceContext = () => useContext(MarketPlaceContext);
MarketPlaceContextProvider.propTypes = {
children: PropTypes.node.isRequired,
downloadPlugin: PropTypes.func.isRequired,
};
export { MarketPlaceContext, MarketPlaceContextProvider, useMarketPlaceContext };

View File

@ -0,0 +1,18 @@
import * as React from 'react';
interface AdminContextValue {
/**
* TODO: this should come from `StrapiApp['getAdminInjectedComponents']`
*/
getAdminInjectedComponents: () => unknown;
}
const AdminContext = React.createContext<AdminContextValue>({
getAdminInjectedComponents() {
throw new Error('AdminContext: getAdminInjectedComponents() not implemented');
},
});
const useAdmin = () => React.useContext(AdminContext);
export { AdminContext, useAdmin };

View File

@ -1 +0,0 @@
export { default as AdminContext } from './Admin';

View File

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

View File

@ -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<Menu>({
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 };

View File

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

View File

@ -1,13 +0,0 @@
import { hasPermissions } from '@strapi/helper-plugin';
/**
* This function resolves an array of Promises<boolean>
* 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <div>menu</div>;
});
jest.mock('../../../components/LeftMenu', () => ({
LeftMenu() {
return <div>menu</div>;
},
}));
jest.mock('../../HomePage', () => () => {
return <div>HomePage</div>;
});

View File

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

View File

@ -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"
>
<span
class="c8 c9 c10"
class="c8 c9"
>
<span
class="c11"
>
English
</span>
class="c10"
/>
</span>
</span>
<span
@ -387,7 +368,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
>
<span
aria-hidden="true"
class="c12"
class="c11"
>
<svg
fill="none"
@ -411,45 +392,45 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
</div>
</header>
<div
class="c13"
class="c12"
>
<main
aria-labelledby="main-content-title"
class="c14"
class="c13"
id="main-content"
tabindex="-1"
>
<div
class="c15 c16"
class="c14 c15"
>
<div
class="c17 c18"
class="c16 c17"
>
<img
alt=""
aria-hidden="true"
class="c19"
src="customAuthLogo.png"
class="c18"
src=""
/>
<div
class="c20"
class="c19"
>
<h1
class="c8 c21"
class="c20"
>
Oops...
</h1>
</div>
<span
class="c8 c22"
class="c21"
>
Your account has been suspended.
</span>
<div
class="c23"
class="c22"
>
<span
class="c8 c22"
class="c21"
>
If this is a mistake, please contact your administrator.
</span>
@ -457,17 +438,17 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
</div>
</div>
<div
class="c24"
class="c23"
>
<div
class="c23"
class="c22"
>
<a
class="c25"
class="c24"
href="/auth/login"
>
<span
class="c8 c26"
class="c25"
>
Sign in
</span>

View File

@ -16,8 +16,6 @@ jest.mock('../../../../../hooks/useConfiguration', () => ({
}),
}));
jest.mock('../../../../../components/LocalesProvider/useLocalesProvider');
describe('AUTH | Register', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@ -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', () => {
>
<span
class="c11"
>
English
</span>
/>
</span>
</span>
<span
@ -753,7 +734,7 @@ describe('ADMIN | PAGES | AUTH | ResetPassword', () => {
alt=""
aria-hidden="true"
class="c19"
src="customAuthLogo.png"
src=""
/>
<div
class="c20"

View File

@ -9,7 +9,7 @@ import PropTypes from 'prop-types';
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
import persistStateToLocaleStorage from '../../components/GuidedTour/utils/persistStateToLocaleStorage';
import useLocalesProvider from '../../components/LocalesProvider/useLocalesProvider';
import { useLocales } from '../../components/LanguageProvider';
import { useEnterprise } from '../../hooks/useEnterprise';
import formatAPIErrors from '../../utils/formatAPIErrors';
@ -23,7 +23,7 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
push,
location: { search },
} = useHistory();
const { changeLocale } = useLocalesProvider();
const { changeLocale } = useLocales();
const { setSkipped } = useGuidedTour();
const { trackUsage } = useTracking();
const {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => {
>
<span
class="c11"
>
English
</span>
/>
</span>
</span>
<span
@ -643,7 +628,7 @@ describe('Admin | UseCasePage', () => {
alt=""
aria-hidden="true"
class="c19"
src="customAuthLogo.png"
src=""
/>
<div
class="c20"

View File

@ -1,2 +1 @@
export { default as useAdminProvider } from './useAdminProvider';
export { default as useInjectionZone } from './useInjectionZone';

View File

@ -1,11 +0,0 @@
import { useContext } from 'react';
import { AdminContext } from '../../../contexts';
const useAdminProvider = () => {
const context = useContext(AdminContext);
return context;
};
export default useAdminProvider;

View File

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

View File

@ -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) => {
<NotificationsProvider>
<RBACContext.Provider
value={{
refetchPermissions: jest.fn(),
allPermissions: [
...fixtures.permissions.allPermissions,
{

View File

@ -0,0 +1,5 @@
############################
# dts
############################
dist/**/*.d.ts.map

View File

@ -23,8 +23,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"source": "./src/index.ts",
"default": "./dist/index.js"
},

View File

@ -9,12 +9,17 @@ type Permission = domain.permission.Permission;
* Context
* -----------------------------------------------------------------------------------------------*/
type RBACContextValue = {
allPermissions?: Permission[]; // The permissions of the current user.
refetchPermissions?: QueryObserverBaseResult<Permission[]>;
export type RBACContextValue = {
allPermissions: Permission[]; // The permissions of the current user.
refetchPermissions: QueryObserverBaseResult<Permission[]>['refetch'];
};
const RBACContext: React.Context<RBACContextValue> = React.createContext({});
const RBACContext = React.createContext<RBACContextValue>({
allPermissions: [],
refetchPermissions: async () => {
throw new Error('RBACContext: refetchPermissions() not implemented');
},
});
/**
* @deprecated Use RBACContext instead.

View File

@ -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<LinkProps, 'to'> {
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;
}

View File

@ -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(),
}),
[]
);

View File

@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"exclude": ["**/*.test.tsx", "**/*.test.ts"],
"compilerOptions": {
"declarationMap": true,
"outDir": "./dist"
}
}