mirror of
https://github.com/strapi/strapi.git
synced 2025-09-04 22:32:57 +00:00
refactor(admin): convert rest of components to TS (#18473)
* refactor(admin): convert rest of components to TS * chore(admin): integrate redux types to admin components * chore: type network requests * test: fix unit tests based on changes * chore: use smaller batch of permissions for RBACProvider test
This commit is contained in:
parent
4b0773bf8c
commit
d4dddebe84
@ -10,23 +10,20 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import Logo from './assets/images/logo-strapi-2022.svg';
|
||||
import { LANGUAGE_LOCAL_STORAGE_KEY } from './components/LanguageProvider';
|
||||
import Providers from './components/Providers';
|
||||
import {
|
||||
HOOKS,
|
||||
INJECTION_ZONES
|
||||
} from './constants';
|
||||
import { Providers } from './components/Providers';
|
||||
import { HOOKS, INJECTION_ZONES } from './constants';
|
||||
import { customFields, Plugin, Reducers } from './core/apis';
|
||||
import { configureStore } from './core/store';
|
||||
import { configureStore } from './core/store/configure';
|
||||
import { basename, createHook } from './core/utils';
|
||||
import favicon from './favicon.png';
|
||||
import App from './pages/App';
|
||||
import languageNativeNames from './translations/languageNativeNames';
|
||||
|
||||
const {
|
||||
INJECT_COLUMN_IN_TABLE,
|
||||
INJECT_COLUMN_IN_TABLE,
|
||||
MUTATE_COLLECTION_TYPES_LINKS,
|
||||
MUTATE_EDIT_VIEW_LAYOUT,
|
||||
MUTATE_SINGLE_TYPES_LINKS
|
||||
MUTATE_SINGLE_TYPES_LINKS,
|
||||
} = HOOKS;
|
||||
|
||||
class StrapiApp {
|
||||
|
184
packages/core/admin/admin/src/components/AuthenticatedApp.tsx
Normal file
184
packages/core/admin/admin/src/components/AuthenticatedApp.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
AppInfoContextValue,
|
||||
AppInfoProvider,
|
||||
auth,
|
||||
LoadingIndicatorPage,
|
||||
useFetchClient,
|
||||
useGuidedTour,
|
||||
} from '@strapi/helper-plugin';
|
||||
import lodashGet from 'lodash/get';
|
||||
import { useQueries } from 'react-query';
|
||||
import lt from 'semver/functions/lt';
|
||||
import valid from 'semver/functions/valid';
|
||||
// TODO: DS add loader
|
||||
|
||||
import packageJSON from '../../../package.json';
|
||||
import { UserEntity } from '../../../shared/entities';
|
||||
import { useConfiguration } from '../hooks/useConfiguration';
|
||||
import { APIResponse, APIResponseUsersLegacy } from '../types/adminAPI';
|
||||
// @ts-expect-error - no types yet.
|
||||
import { getFullName, hashAdminUserEmail } from '../utils';
|
||||
|
||||
import { NpsSurvey } from './NpsSurvey';
|
||||
import { PluginsInitializer } from './PluginsInitializer';
|
||||
import { RBACProvider, Permission } from './RBACProvider';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
const AuthenticatedApp = () => {
|
||||
const { setGuidedTourVisibility } = useGuidedTour();
|
||||
const userInfo = auth.get('userInfo');
|
||||
const userName = userInfo
|
||||
? lodashGet(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname)
|
||||
: null;
|
||||
const [userDisplayName, setUserDisplayName] = React.useState(userName);
|
||||
const [userId, setUserId] = React.useState<string>();
|
||||
const { showReleaseNotification } = useConfiguration();
|
||||
const { get } = useFetchClient();
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
{ data: tagName, isLoading },
|
||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
|
||||
{ data: userRoles },
|
||||
] = useQueries([
|
||||
{
|
||||
queryKey: 'app-infos',
|
||||
async queryFn() {
|
||||
const { data } = await get<
|
||||
APIResponse<
|
||||
Pick<
|
||||
AppInfoContextValue,
|
||||
| 'currentEnvironment'
|
||||
| 'autoReload'
|
||||
| 'communityEdition'
|
||||
| 'dependencies'
|
||||
| 'useYarn'
|
||||
| 'projectId'
|
||||
| 'strapiVersion'
|
||||
| 'nodeVersion'
|
||||
>
|
||||
>
|
||||
>('/admin/information');
|
||||
|
||||
return data.data;
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: 'strapi-release',
|
||||
async queryFn() {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/strapi/strapi/releases/latest');
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const response = (await res.json()) as { tag_name: string | null | undefined };
|
||||
|
||||
if (!response.tag_name) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return response.tag_name;
|
||||
} catch (err) {
|
||||
// Don't throw an error
|
||||
return strapiVersion;
|
||||
}
|
||||
},
|
||||
enabled: showReleaseNotification,
|
||||
initialData: strapiVersion,
|
||||
},
|
||||
{
|
||||
queryKey: 'admin-users-permission',
|
||||
async queryFn() {
|
||||
const { data } = await get<{ data: Permission[] }>('/admin/users/me/permissions');
|
||||
|
||||
return data.data;
|
||||
},
|
||||
initialData: [],
|
||||
},
|
||||
{
|
||||
queryKey: 'user-roles',
|
||||
async queryFn() {
|
||||
const {
|
||||
data: {
|
||||
data: { roles },
|
||||
},
|
||||
} = await get<APIResponseUsersLegacy<UserEntity>>('/admin/users/me');
|
||||
|
||||
return roles;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldUpdateStrapi = checkLatestStrapiVersion(strapiVersion, tagName);
|
||||
|
||||
/**
|
||||
* TODO: does this actually need to be an effect?
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (userRoles) {
|
||||
const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin');
|
||||
|
||||
if (isUserSuperAdmin && appInfos?.autoReload) {
|
||||
setGuidedTourVisibility(true);
|
||||
}
|
||||
}
|
||||
}, [userRoles, appInfos, setGuidedTourVisibility]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const getUserId = async () => {
|
||||
const userId = await hashAdminUserEmail(userInfo);
|
||||
setUserId(userId);
|
||||
};
|
||||
|
||||
getUserId();
|
||||
}, [userInfo]);
|
||||
|
||||
// We don't need to wait for the release query to be fetched before rendering the plugins
|
||||
// however, we need the appInfos and the permissions
|
||||
const shouldShowNotDependentQueriesLoader =
|
||||
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||
|
||||
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
// TODO: add error state
|
||||
if (status === 'error') {
|
||||
return <div>error...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppInfoProvider
|
||||
{...appInfos}
|
||||
userId={userId}
|
||||
latestStrapiReleaseTag={tagName}
|
||||
setUserDisplayName={setUserDisplayName}
|
||||
shouldUpdateStrapi={shouldUpdateStrapi}
|
||||
userDisplayName={userDisplayName}
|
||||
>
|
||||
<RBACProvider permissions={permissions ?? []} refetchPermissions={refetch}>
|
||||
<NpsSurvey />
|
||||
<PluginsInitializer />
|
||||
</RBACProvider>
|
||||
</AppInfoProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const checkLatestStrapiVersion = (
|
||||
currentPackageVersion: string,
|
||||
latestPublishedVersion: string = ''
|
||||
): boolean => {
|
||||
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lt(currentPackageVersion, latestPublishedVersion);
|
||||
};
|
||||
|
||||
export { AuthenticatedApp };
|
@ -1,116 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
AppInfoProvider,
|
||||
auth,
|
||||
LoadingIndicatorPage,
|
||||
useGuidedTour,
|
||||
useNotification,
|
||||
} from '@strapi/helper-plugin';
|
||||
import get from 'lodash/get';
|
||||
import { useQueries } from 'react-query';
|
||||
// TODO: DS add loader
|
||||
|
||||
import packageJSON from '../../../../package.json';
|
||||
import { useConfiguration } from '../../hooks/useConfiguration';
|
||||
import { getFullName, hashAdminUserEmail } from '../../utils';
|
||||
import { NpsSurvey } from '../NpsSurvey';
|
||||
import { PluginsInitializer } from '../PluginsInitializer';
|
||||
import RBACProvider from '../RBACProvider';
|
||||
|
||||
import { fetchAppInfo, fetchCurrentUserPermissions, fetchUserRoles } from './utils/api';
|
||||
import { checkLatestStrapiVersion } from './utils/checkLatestStrapiVersion';
|
||||
import { fetchStrapiLatestRelease } from './utils/fetchStrapiLatestRelease';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
const AuthenticatedApp = () => {
|
||||
const { setGuidedTourVisibility } = useGuidedTour();
|
||||
const toggleNotification = useNotification();
|
||||
const userInfo = auth.getUserInfo();
|
||||
const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname);
|
||||
const [userDisplayName, setUserDisplayName] = useState(userName);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const { showReleaseNotification } = useConfiguration();
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
{ data: tagName, isLoading },
|
||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
|
||||
{ data: userRoles },
|
||||
] = useQueries([
|
||||
{ queryKey: 'app-infos', queryFn: fetchAppInfo },
|
||||
{
|
||||
queryKey: 'strapi-release',
|
||||
queryFn: () => fetchStrapiLatestRelease(toggleNotification),
|
||||
enabled: showReleaseNotification,
|
||||
initialData: strapiVersion,
|
||||
},
|
||||
{
|
||||
queryKey: 'admin-users-permission',
|
||||
queryFn: fetchCurrentUserPermissions,
|
||||
initialData: [],
|
||||
},
|
||||
{
|
||||
queryKey: 'user-roles',
|
||||
queryFn: fetchUserRoles,
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldUpdateStrapi = checkLatestStrapiVersion(strapiVersion, tagName);
|
||||
|
||||
/**
|
||||
* TODO: does this actually need to be an effect?
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (userRoles) {
|
||||
const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin');
|
||||
|
||||
if (isUserSuperAdmin && appInfos?.autoReload) {
|
||||
setGuidedTourVisibility(true);
|
||||
}
|
||||
}
|
||||
}, [userRoles, appInfos, setGuidedTourVisibility]);
|
||||
|
||||
useEffect(() => {
|
||||
const getUserId = async () => {
|
||||
const userId = await hashAdminUserEmail(userInfo);
|
||||
setUserId(userId);
|
||||
};
|
||||
|
||||
getUserId();
|
||||
}, [userInfo]);
|
||||
|
||||
// We don't need to wait for the release query to be fetched before rendering the plugins
|
||||
// however, we need the appInfos and the permissions
|
||||
const shouldShowNotDependentQueriesLoader =
|
||||
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||
|
||||
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
// TODO: add error state
|
||||
if (status === 'error') {
|
||||
return <div>error...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppInfoProvider
|
||||
{...appInfos}
|
||||
userId={userId}
|
||||
latestStrapiReleaseTag={tagName}
|
||||
setUserDisplayName={setUserDisplayName}
|
||||
shouldUpdateStrapi={shouldUpdateStrapi}
|
||||
userDisplayName={userDisplayName}
|
||||
>
|
||||
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
||||
<NpsSurvey />
|
||||
<PluginsInitializer />
|
||||
</RBACProvider>
|
||||
</AppInfoProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatedApp;
|
@ -1,47 +0,0 @@
|
||||
import { getFetchClient } from '@strapi/helper-plugin';
|
||||
|
||||
const { get } = getFetchClient();
|
||||
|
||||
const fetchAppInfo = async () => {
|
||||
try {
|
||||
const { data, headers } = await get('/admin/information');
|
||||
|
||||
if (!headers['content-type'].includes('application/json')) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrentUserPermissions = async () => {
|
||||
try {
|
||||
const { data, headers } = await get('/admin/users/me/permissions');
|
||||
|
||||
if (!headers['content-type'].includes('application/json')) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserRoles = async () => {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
data: { roles },
|
||||
},
|
||||
} = await get('/admin/users/me');
|
||||
|
||||
return roles;
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchAppInfo, fetchCurrentUserPermissions, fetchUserRoles };
|
@ -1,13 +0,0 @@
|
||||
import lt from 'semver/functions/lt';
|
||||
import valid from 'semver/functions/valid';
|
||||
|
||||
export const checkLatestStrapiVersion = (
|
||||
currentPackageVersion: string,
|
||||
latestPublishedVersion: string
|
||||
): boolean => {
|
||||
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lt(currentPackageVersion, latestPublishedVersion);
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
import packageJSON from '../../../../../package.json';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
export const fetchStrapiLatestRelease = async () => {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/strapi/strapi/releases/latest');
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch latest Strapi version.');
|
||||
}
|
||||
const { tag_name } = await res.json();
|
||||
|
||||
return tag_name;
|
||||
} catch (err) {
|
||||
// Don't throw an error
|
||||
return strapiVersion;
|
||||
}
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
import { checkLatestStrapiVersion } from '../checkLatestStrapiVersion';
|
||||
|
||||
describe('ADMIN | utils | checkLatestStrapiVersion', () => {
|
||||
it('should return true if the current version is lower than the latest published version', () => {
|
||||
expect(checkLatestStrapiVersion('v3.3.2', 'v3.3.4')).toBeTruthy();
|
||||
expect(checkLatestStrapiVersion('3.3.2', 'v3.3.4')).toBeTruthy();
|
||||
expect(checkLatestStrapiVersion('v3.3.2', '3.3.4')).toBeTruthy();
|
||||
expect(checkLatestStrapiVersion('3.3.2', '3.3.4')).toBeTruthy();
|
||||
});
|
||||
it('should return false if the current version is equal to the latest published version', () => {
|
||||
expect(checkLatestStrapiVersion('3.3.4', 'v3.3.4')).toBeFalsy();
|
||||
expect(checkLatestStrapiVersion('v3.3.4', '3.3.4')).toBeFalsy();
|
||||
expect(checkLatestStrapiVersion('3.3.4', '3.3.4')).toBeFalsy();
|
||||
});
|
||||
it('should return false if the current version is a beta of the next release', () => {
|
||||
expect(checkLatestStrapiVersion('3.4.0-beta.1', 'v3.3.4')).toBeFalsy();
|
||||
expect(checkLatestStrapiVersion('v3.4.0-beta.1', '3.3.4')).toBeFalsy();
|
||||
expect(checkLatestStrapiVersion('3.4.0-beta.1', '3.3.4')).toBeFalsy();
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { ConfigurationContext, ConfigurationContextValue } from '../contexts/configuration';
|
||||
|
||||
export interface ConfigurationProviderProps {
|
||||
interface ConfigurationProviderProps {
|
||||
children: React.ReactNode;
|
||||
authLogo: string;
|
||||
menuLogo: string;
|
||||
@ -65,3 +65,4 @@ const ConfigurationProvider = ({
|
||||
};
|
||||
|
||||
export { ConfigurationProvider };
|
||||
export type { ConfigurationProviderProps };
|
||||
|
@ -127,3 +127,4 @@ const reducer = (state = initialState, action: Action) => {
|
||||
};
|
||||
|
||||
export { LanguageProvider, useLocales, LANGUAGE_LOCAL_STORAGE_KEY };
|
||||
export type { LanguageProviderProps, LocalesContextValue };
|
||||
|
125
packages/core/admin/admin/src/components/Providers.tsx
Normal file
125
packages/core/admin/admin/src/components/Providers.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
AutoReloadOverlayBlockerProvider,
|
||||
CustomFieldsProvider,
|
||||
CustomFieldsProviderProps,
|
||||
LibraryProvider,
|
||||
LibraryProviderProps,
|
||||
NotificationsProvider,
|
||||
OverlayBlockerProvider,
|
||||
StrapiAppProvider,
|
||||
StrapiAppProviderProps,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { AdminContext, AdminContextValue } from '../contexts/admin';
|
||||
|
||||
import { ConfigurationProvider, ConfigurationProviderProps } from './ConfigurationProvider';
|
||||
import { GuidedTourProvider } from './GuidedTour/Provider';
|
||||
import { LanguageProvider, LanguageProviderProps } from './LanguageProvider';
|
||||
import { Theme } from './Theme';
|
||||
import { ThemeToggleProvider, ThemeToggleProviderProps } from './ThemeToggleProvider';
|
||||
|
||||
import type { Store } from '../core/store/configure';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface ProvidersProps
|
||||
extends Pick<ThemeToggleProviderProps, 'themes'>,
|
||||
Pick<LanguageProviderProps, 'messages' | 'localeNames'>,
|
||||
Pick<
|
||||
ConfigurationProviderProps,
|
||||
'authLogo' | 'menuLogo' | 'showReleaseNotification' | 'showTutorials'
|
||||
>,
|
||||
Pick<AdminContextValue, 'getAdminInjectedComponents'>,
|
||||
Pick<CustomFieldsProviderProps, 'customFields'>,
|
||||
Pick<LibraryProviderProps, 'components' | 'fields'>,
|
||||
Pick<
|
||||
StrapiAppProviderProps,
|
||||
| 'getPlugin'
|
||||
| 'menu'
|
||||
| 'plugins'
|
||||
| 'runHookParallel'
|
||||
| 'runHookSeries'
|
||||
| 'runHookWaterfall'
|
||||
| 'settings'
|
||||
> {
|
||||
children: React.ReactNode;
|
||||
store: Store;
|
||||
}
|
||||
|
||||
const Providers = ({
|
||||
authLogo,
|
||||
children,
|
||||
components,
|
||||
customFields,
|
||||
fields,
|
||||
getAdminInjectedComponents,
|
||||
getPlugin,
|
||||
localeNames,
|
||||
menu,
|
||||
menuLogo,
|
||||
messages,
|
||||
plugins,
|
||||
runHookParallel,
|
||||
runHookSeries,
|
||||
runHookWaterfall,
|
||||
settings,
|
||||
showReleaseNotification,
|
||||
showTutorials,
|
||||
store,
|
||||
themes,
|
||||
}: ProvidersProps) => {
|
||||
return (
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<ThemeToggleProvider themes={themes}>
|
||||
<Theme>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
|
||||
<ConfigurationProvider
|
||||
authLogo={authLogo}
|
||||
menuLogo={menuLogo}
|
||||
showReleaseNotification={showReleaseNotification}
|
||||
showTutorials={showTutorials}
|
||||
>
|
||||
<StrapiAppProvider
|
||||
getPlugin={getPlugin}
|
||||
menu={menu}
|
||||
plugins={plugins}
|
||||
runHookParallel={runHookParallel}
|
||||
runHookWaterfall={runHookWaterfall}
|
||||
runHookSeries={runHookSeries}
|
||||
settings={settings}
|
||||
>
|
||||
<LibraryProvider components={components} fields={fields}>
|
||||
<CustomFieldsProvider customFields={customFields}>
|
||||
<AutoReloadOverlayBlockerProvider>
|
||||
<OverlayBlockerProvider>
|
||||
<GuidedTourProvider>
|
||||
<NotificationsProvider>{children}</NotificationsProvider>
|
||||
</GuidedTourProvider>
|
||||
</OverlayBlockerProvider>
|
||||
</AutoReloadOverlayBlockerProvider>
|
||||
</CustomFieldsProvider>
|
||||
</LibraryProvider>
|
||||
</StrapiAppProvider>
|
||||
</ConfigurationProvider>
|
||||
</AdminContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Theme>
|
||||
</ThemeToggleProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { Providers };
|
@ -1,156 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
AutoReloadOverlayBlockerProvider,
|
||||
CustomFieldsProvider,
|
||||
LibraryProvider,
|
||||
NotificationsProvider,
|
||||
OverlayBlockerProvider,
|
||||
StrapiAppProvider,
|
||||
} from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { AdminContext } from '../../contexts/admin';
|
||||
import { ConfigurationProvider } from '../ConfigurationProvider';
|
||||
import { GuidedTourProvider } from '../GuidedTour/Provider';
|
||||
import { LanguageProvider } from '../LanguageProvider';
|
||||
import { Theme } from '../Theme';
|
||||
import { ThemeToggleProvider } from '../ThemeToggleProvider';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Providers = ({
|
||||
authLogo,
|
||||
children,
|
||||
components,
|
||||
customFields,
|
||||
fields,
|
||||
getAdminInjectedComponents,
|
||||
getPlugin,
|
||||
localeNames,
|
||||
menu,
|
||||
menuLogo,
|
||||
messages,
|
||||
plugins,
|
||||
runHookParallel,
|
||||
runHookSeries,
|
||||
runHookWaterfall,
|
||||
settings,
|
||||
showReleaseNotification,
|
||||
showTutorials,
|
||||
store,
|
||||
themes,
|
||||
}) => {
|
||||
return (
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<ThemeToggleProvider themes={themes}>
|
||||
<Theme>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
|
||||
<ConfigurationProvider
|
||||
authLogo={authLogo}
|
||||
menuLogo={menuLogo}
|
||||
showReleaseNotification={showReleaseNotification}
|
||||
showTutorials={showTutorials}
|
||||
>
|
||||
<StrapiAppProvider
|
||||
getPlugin={getPlugin}
|
||||
menu={menu}
|
||||
plugins={plugins}
|
||||
runHookParallel={runHookParallel}
|
||||
runHookWaterfall={runHookWaterfall}
|
||||
runHookSeries={runHookSeries}
|
||||
settings={settings}
|
||||
>
|
||||
<LibraryProvider components={components} fields={fields}>
|
||||
<CustomFieldsProvider customFields={customFields}>
|
||||
<AutoReloadOverlayBlockerProvider>
|
||||
<OverlayBlockerProvider>
|
||||
<GuidedTourProvider>
|
||||
<NotificationsProvider>{children}</NotificationsProvider>
|
||||
</GuidedTourProvider>
|
||||
</OverlayBlockerProvider>
|
||||
</AutoReloadOverlayBlockerProvider>
|
||||
</CustomFieldsProvider>
|
||||
</LibraryProvider>
|
||||
</StrapiAppProvider>
|
||||
</ConfigurationProvider>
|
||||
</AdminContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Theme>
|
||||
</ThemeToggleProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Providers.propTypes = {
|
||||
authLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
components: PropTypes.object.isRequired,
|
||||
customFields: PropTypes.object.isRequired,
|
||||
fields: PropTypes.object.isRequired,
|
||||
getAdminInjectedComponents: PropTypes.func.isRequired,
|
||||
getPlugin: PropTypes.func.isRequired,
|
||||
localeNames: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
menu: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
to: PropTypes.string.isRequired,
|
||||
icon: PropTypes.func.isRequired,
|
||||
intlLabel: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
permissions: PropTypes.array,
|
||||
Component: PropTypes.func,
|
||||
})
|
||||
).isRequired,
|
||||
menuLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
plugins: PropTypes.object.isRequired,
|
||||
runHookParallel: PropTypes.func.isRequired,
|
||||
runHookWaterfall: PropTypes.func.isRequired,
|
||||
runHookSeries: PropTypes.func.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
showReleaseNotification: PropTypes.bool.isRequired,
|
||||
showTutorials: PropTypes.bool.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
themes: PropTypes.shape({
|
||||
light: PropTypes.shape({
|
||||
colors: PropTypes.object.isRequired,
|
||||
shadows: PropTypes.object.isRequired,
|
||||
sizes: PropTypes.object.isRequired,
|
||||
zIndices: PropTypes.array.isRequired,
|
||||
spaces: PropTypes.array.isRequired,
|
||||
borderRadius: PropTypes.string.isRequired,
|
||||
mediaQueries: PropTypes.object.isRequired,
|
||||
fontSizes: PropTypes.array.isRequired,
|
||||
lineHeights: PropTypes.array.isRequired,
|
||||
fontWeights: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
dark: PropTypes.shape({
|
||||
colors: PropTypes.object.isRequired,
|
||||
shadows: PropTypes.object.isRequired,
|
||||
sizes: PropTypes.object.isRequired,
|
||||
zIndices: PropTypes.array.isRequired,
|
||||
spaces: PropTypes.array.isRequired,
|
||||
borderRadius: PropTypes.string.isRequired,
|
||||
mediaQueries: PropTypes.object.isRequired,
|
||||
fontSizes: PropTypes.array.isRequired,
|
||||
lineHeights: PropTypes.array.isRequired,
|
||||
fontWeights: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
custom: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default Providers;
|
124
packages/core/admin/admin/src/components/RBACProvider.tsx
Normal file
124
packages/core/admin/admin/src/components/RBACProvider.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
LoadingIndicatorPage,
|
||||
Permission,
|
||||
RBACContext,
|
||||
RBACContextValue,
|
||||
} from '@strapi/helper-plugin';
|
||||
import produce from 'immer';
|
||||
|
||||
import { useTypedSelector, useTypedDispatch } from '../core/store/hooks';
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* RBACProvider
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface RBACProviderProps {
|
||||
children: React.ReactNode;
|
||||
permissions: Permission[];
|
||||
refetchPermissions: RBACContextValue['refetchPermissions'];
|
||||
}
|
||||
|
||||
const RBACProvider = ({ children, permissions, refetchPermissions }: RBACProviderProps) => {
|
||||
const allPermissions = useTypedSelector((state) => state.rbacProvider.allPermissions);
|
||||
|
||||
const dispatch = useTypedDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(setPermissionsAction(permissions));
|
||||
|
||||
return () => {
|
||||
dispatch(resetStoreAction());
|
||||
};
|
||||
}, [permissions, dispatch]);
|
||||
|
||||
if (!allPermissions) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RBACContext.Provider value={{ allPermissions, refetchPermissions }}>
|
||||
{children}
|
||||
</RBACContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* RBACReducer
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface RBACState {
|
||||
allPermissions: null | Permission[];
|
||||
collectionTypesRelatedPermissions: Record<string, Record<string, Permission[]>>;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
allPermissions: null,
|
||||
collectionTypesRelatedPermissions: {},
|
||||
};
|
||||
|
||||
const RESET_STORE = 'StrapiAdmin/RBACProvider/RESET_STORE';
|
||||
const SET_PERMISSIONS = 'StrapiAdmin/RBACProvider/SET_PERMISSIONS';
|
||||
|
||||
interface ResetStoreAction {
|
||||
type: typeof RESET_STORE;
|
||||
}
|
||||
|
||||
const resetStoreAction = (): ResetStoreAction => ({ type: RESET_STORE });
|
||||
|
||||
interface SetPermissionsAction {
|
||||
type: typeof SET_PERMISSIONS;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
const setPermissionsAction = (
|
||||
permissions: SetPermissionsAction['permissions']
|
||||
): SetPermissionsAction => ({
|
||||
type: SET_PERMISSIONS,
|
||||
permissions,
|
||||
});
|
||||
|
||||
type Actions = ResetStoreAction | SetPermissionsAction;
|
||||
|
||||
const RBACReducer = (state: RBACState = initialState, action: Actions) =>
|
||||
produce(state, (draftState) => {
|
||||
switch (action.type) {
|
||||
case SET_PERMISSIONS: {
|
||||
draftState.allPermissions = action.permissions;
|
||||
draftState.collectionTypesRelatedPermissions = action.permissions
|
||||
.filter((perm) => perm.subject)
|
||||
.reduce<Record<string, Record<string, Permission[]>>>((acc, current) => {
|
||||
const { subject, action } = current;
|
||||
|
||||
if (!subject) return acc;
|
||||
|
||||
if (!acc[subject]) {
|
||||
acc[subject] = {};
|
||||
}
|
||||
|
||||
acc[subject] = acc[subject][action]
|
||||
? { ...acc[subject], [action]: [...acc[subject][action], current] }
|
||||
: { ...acc[subject], [action]: [current] };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
break;
|
||||
}
|
||||
case RESET_STORE: {
|
||||
return initialState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
export { RBACProvider, RBACReducer, resetStoreAction, setPermissionsAction };
|
||||
export type {
|
||||
RBACState,
|
||||
Actions,
|
||||
RBACProviderProps,
|
||||
ResetStoreAction,
|
||||
SetPermissionsAction,
|
||||
Permission,
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { RESET_STORE, SET_PERMISSIONS } from './constants';
|
||||
|
||||
const resetStore = () => ({ type: RESET_STORE });
|
||||
|
||||
const setPermissions = (permissions) => ({
|
||||
type: SET_PERMISSIONS,
|
||||
permissions,
|
||||
});
|
||||
|
||||
export { resetStore, setPermissions };
|
@ -1,2 +0,0 @@
|
||||
export const RESET_STORE = 'StrapiAdmin/RBACProvider/RESET_STORE';
|
||||
export const SET_PERMISSIONS = 'StrapiAdmin/RBACProvider/SET_PERMISSIONS';
|
@ -1,39 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { LoadingIndicatorPage, RBACProviderContext } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { resetStore, setPermissions } from './actions';
|
||||
|
||||
const RBACProvider = ({ children, permissions, refetchPermissions }) => {
|
||||
const { allPermissions } = useSelector((state) => state.rbacProvider);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPermissions(permissions));
|
||||
|
||||
return () => {
|
||||
dispatch(resetStore());
|
||||
};
|
||||
}, [permissions, dispatch]);
|
||||
|
||||
if (!allPermissions) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RBACProviderContext.Provider value={{ allPermissions, refetchPermissions }}>
|
||||
{children}
|
||||
</RBACProviderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
RBACProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
permissions: PropTypes.array.isRequired,
|
||||
refetchPermissions: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RBACProvider;
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* RBACProvider reducer
|
||||
* The goal of this reducer is to provide
|
||||
* the plugins with an access to the user's permissions
|
||||
* in our middleware system
|
||||
*
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
|
||||
import { RESET_STORE, SET_PERMISSIONS } from './constants';
|
||||
|
||||
const initialState = {
|
||||
allPermissions: null,
|
||||
collectionTypesRelatedPermissions: {},
|
||||
};
|
||||
|
||||
const reducer = (state = initialState, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, (draftState) => {
|
||||
switch (action.type) {
|
||||
case SET_PERMISSIONS: {
|
||||
draftState.allPermissions = action.permissions;
|
||||
draftState.collectionTypesRelatedPermissions = action.permissions
|
||||
.filter((perm) => perm.subject)
|
||||
.reduce((acc, current) => {
|
||||
const { subject, action } = current;
|
||||
|
||||
if (!acc[subject]) {
|
||||
acc[subject] = {};
|
||||
}
|
||||
|
||||
acc[subject] = acc[subject][action]
|
||||
? { ...acc[subject], [action]: [...acc[subject][action], current] }
|
||||
: { ...acc[subject], [action]: [current] };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
break;
|
||||
}
|
||||
case RESET_STORE: {
|
||||
return initialState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
export { initialState };
|
@ -48,3 +48,4 @@ const ThemeToggleProvider = ({ children, themes }: ThemeToggleProviderProps) =>
|
||||
};
|
||||
|
||||
export { ThemeToggleProvider };
|
||||
export type { ThemeToggleProviderProps };
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, waitFor } from '@tests/utils';
|
||||
|
||||
import AuthenticatedApp from '../index';
|
||||
import { AuthenticatedApp } from '../AuthenticatedApp';
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
@ -11,14 +9,14 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
*/
|
||||
usePersistentState: jest.fn().mockImplementation(() => [{ enabled: false }, jest.fn()]),
|
||||
auth: {
|
||||
getUserInfo: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
|
||||
get: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
|
||||
},
|
||||
useGuidedTour: jest.fn(() => ({
|
||||
setGuidedTourVisibility: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../PluginsInitializer', () => ({
|
||||
jest.mock('../PluginsInitializer', () => ({
|
||||
PluginsInitializer() {
|
||||
return <div>PluginsInitializer</div>;
|
||||
},
|
||||
@ -34,10 +32,10 @@ describe('AuthenticatedApp', () => {
|
||||
});
|
||||
|
||||
it('should not crash', async () => {
|
||||
const { queryByText } = render(<AuthenticatedApp />);
|
||||
const { queryByText, getByText } = render(<AuthenticatedApp />);
|
||||
|
||||
await waitFor(() => expect(queryByText(/Loading/)).not.toBeInTheDocument());
|
||||
|
||||
expect(queryByText(/PluginsInitializer/)).toBeInTheDocument();
|
||||
expect(getByText(/PluginsInitializer/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,10 +1,15 @@
|
||||
import { fixtures } from '@strapi/admin-test-utils';
|
||||
|
||||
import { resetStore, setPermissions } from '../actions';
|
||||
import rbacProviderReducer, { initialState } from '../reducer';
|
||||
import {
|
||||
Permission,
|
||||
RBACReducer,
|
||||
RBACState,
|
||||
resetStoreAction,
|
||||
setPermissionsAction,
|
||||
} from '../RBACProvider';
|
||||
|
||||
describe('rbacProviderReducer', () => {
|
||||
let state;
|
||||
describe('RBACReducer', () => {
|
||||
let state: RBACState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
@ -16,24 +21,41 @@ describe('rbacProviderReducer', () => {
|
||||
it('returns the initial state', () => {
|
||||
const expected = state;
|
||||
|
||||
expect(rbacProviderReducer(undefined, {})).toEqual(expected);
|
||||
// @ts-expect-error – testing the default case
|
||||
expect(RBACReducer(undefined, {})).toEqual(expected);
|
||||
});
|
||||
|
||||
describe('resetStore', () => {
|
||||
describe('resetStoreAction', () => {
|
||||
it('should reset the state to its initial value', () => {
|
||||
state.allPermissions = true;
|
||||
state.collectionTypesRelatedPermissions = true;
|
||||
state.allPermissions = [];
|
||||
state.collectionTypesRelatedPermissions = {
|
||||
apple: {},
|
||||
};
|
||||
|
||||
expect(rbacProviderReducer(state, resetStore())).toEqual(initialState);
|
||||
expect(RBACReducer(state, resetStoreAction())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"allPermissions": null,
|
||||
"collectionTypesRelatedPermissions": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPermissions', () => {
|
||||
describe('setPermissionsAction', () => {
|
||||
it('should set the allPermissions value correctly', () => {
|
||||
const permissions = [{ action: 'test', subject: null }];
|
||||
const permissions: Permission[] = [
|
||||
{
|
||||
id: 0,
|
||||
action: 'test',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
properties: {},
|
||||
actionParameters: {},
|
||||
},
|
||||
];
|
||||
const expected = { ...state, allPermissions: permissions };
|
||||
|
||||
expect(rbacProviderReducer(state, setPermissions(permissions))).toEqual(expected);
|
||||
expect(RBACReducer(state, setPermissionsAction(permissions))).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should set the collectionTypesRelatedPermissions correctly', () => {
|
||||
@ -89,7 +111,7 @@ describe('rbacProviderReducer', () => {
|
||||
};
|
||||
|
||||
expect(
|
||||
rbacProviderReducer(state, setPermissions(fixtures.permissions.allPermissions))
|
||||
RBACReducer(state, setPermissionsAction(fixtures.permissions.contentManager))
|
||||
.collectionTypesRelatedPermissions
|
||||
).toEqual(expected);
|
||||
});
|
@ -44,7 +44,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useLocation, Link as ReactRouterLink } from 'react-router-dom';
|
||||
|
||||
import { HOOKS } from '../../../constants';
|
||||
import { useTypedSelector } from '../../../core/store';
|
||||
import { useTypedSelector } from '../../../core/store/hooks';
|
||||
import { useAdminUsers } from '../../../hooks/useAdminUsers';
|
||||
import { useEnterprise } from '../../../hooks/useEnterprise';
|
||||
import { InjectionZone } from '../../../shared/components';
|
||||
|
@ -16,3 +16,4 @@ const AdminContext = React.createContext<AdminContextValue>({
|
||||
const useAdmin = () => React.useContext(AdminContext);
|
||||
|
||||
export { AdminContext, useAdmin };
|
||||
export type { AdminContextValue };
|
||||
|
@ -4,42 +4,28 @@ import {
|
||||
Middleware,
|
||||
Reducer,
|
||||
combineReducers,
|
||||
createSelector,
|
||||
Selector,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { useDispatch, useStore, TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
|
||||
import { RBACReducer } from '../../components/RBACProvider';
|
||||
// @ts-expect-error no types, yet.
|
||||
import rbacProviderReducer from '../components/RBACProvider/reducer';
|
||||
import rbacManagerReducer from '../../content-manager/hooks/useSyncRbac/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import rbacManagerReducer from '../content-manager/hooks/useSyncRbac/reducer';
|
||||
import cmAppReducer from '../../content-manager/pages/App/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import cmAppReducer from '../content-manager/pages/App/reducer';
|
||||
import editViewLayoutManagerReducer from '../../content-manager/pages/EditViewLayoutManager/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import editViewLayoutManagerReducer from '../content-manager/pages/EditViewLayoutManager/reducer';
|
||||
import listViewReducer from '../../content-manager/pages/ListView/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import listViewReducer from '../content-manager/pages/ListView/reducer';
|
||||
import editViewCrudReducer from '../../content-manager/sharedReducers/crudReducer/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import editViewCrudReducer from '../content-manager/sharedReducers/crudReducer/reducer';
|
||||
// @ts-expect-error no types, yet.
|
||||
import appReducer from '../pages/App/reducer';
|
||||
|
||||
const createReducer = (
|
||||
appReducers: Record<string, Reducer>,
|
||||
asyncReducers: Record<string, Reducer>
|
||||
) => {
|
||||
return combineReducers({
|
||||
...appReducers,
|
||||
...asyncReducers,
|
||||
});
|
||||
};
|
||||
import appReducer from '../../pages/App/reducer';
|
||||
|
||||
/**
|
||||
* @description Static reducers are ones we know, they live in the admin package.
|
||||
*/
|
||||
const staticReducers: Record<string, Reducer> = {
|
||||
const staticReducers = {
|
||||
admin_app: appReducer,
|
||||
rbacProvider: rbacProviderReducer,
|
||||
rbacProvider: RBACReducer,
|
||||
'content-manager_app': cmAppReducer,
|
||||
'content-manager_listView': listViewReducer,
|
||||
'content-manager_rbacManager': rbacManagerReducer,
|
||||
@ -60,8 +46,13 @@ const injectReducerStoreEnhancer: (appReducers: Record<string, Reducer>) => Stor
|
||||
asyncReducers,
|
||||
injectReducer: (key: string, asyncReducer: Reducer) => {
|
||||
asyncReducers[key] = asyncReducer;
|
||||
// @ts-expect-error we dynamically add reducers which makes the types uncomfortable.
|
||||
store.replaceReducer(createReducer(appReducers, asyncReducers));
|
||||
store.replaceReducer(
|
||||
// @ts-expect-error we dynamically add reducers which makes the types uncomfortable.
|
||||
combineReducers({
|
||||
...appReducers,
|
||||
...asyncReducers,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -74,10 +65,10 @@ const configureStoreImpl = (
|
||||
appMiddlewares: Array<() => Middleware> = [],
|
||||
injectedReducers: Record<string, Reducer> = {}
|
||||
) => {
|
||||
const coreReducers = { ...staticReducers, ...injectedReducers };
|
||||
const coreReducers = { ...staticReducers, ...injectedReducers } as const;
|
||||
|
||||
const store = configureStore({
|
||||
reducer: createReducer(coreReducers, {}),
|
||||
reducer: coreReducers,
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
middleware: (getDefaultMiddleware) => [
|
||||
...getDefaultMiddleware(),
|
||||
@ -95,20 +86,6 @@ type Store = ReturnType<typeof configureStoreImpl> & {
|
||||
};
|
||||
|
||||
type RootState = ReturnType<Store['getState']>;
|
||||
type AppDispatch = Store['dispatch'];
|
||||
|
||||
const useTypedDispatch: () => AppDispatch = useDispatch;
|
||||
const useTypedStore = useStore as () => Store;
|
||||
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
const createTypedSelector = <TResult>(selector: Selector<RootState, TResult>) =>
|
||||
createSelector((state: RootState) => state, selector);
|
||||
|
||||
export {
|
||||
useTypedDispatch,
|
||||
useTypedStore,
|
||||
useTypedSelector,
|
||||
configureStoreImpl as configureStore,
|
||||
createTypedSelector,
|
||||
};
|
||||
export type { RootState };
|
||||
export { configureStoreImpl as configureStore };
|
||||
export type { RootState, Store };
|
15
packages/core/admin/admin/src/core/store/hooks.ts
Normal file
15
packages/core/admin/admin/src/core/store/hooks.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createSelector, Selector } from '@reduxjs/toolkit';
|
||||
import { useDispatch, useStore, TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
|
||||
import type { RootState, Store } from './configure';
|
||||
|
||||
type AppDispatch = Store['dispatch'];
|
||||
|
||||
const useTypedDispatch: () => AppDispatch = useDispatch;
|
||||
const useTypedStore = useStore as () => Store;
|
||||
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
const createTypedSelector = <TResult>(selector: Selector<RootState, TResult>) =>
|
||||
createSelector((state: RootState) => state, selector);
|
||||
|
||||
export { useTypedDispatch, useTypedStore, useTypedSelector, createTypedSelector };
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { configureStore } from '../../../core/store';
|
||||
import { configureStore } from '../../../core/store/configure';
|
||||
import { useInjectReducer } from '../useInjectReducer';
|
||||
|
||||
const store = configureStore();
|
||||
|
@ -33,7 +33,9 @@ import UseCasePage from '../UseCasePage';
|
||||
import { ROUTES_CE, SET_ADMIN_PERMISSIONS } from './constants';
|
||||
|
||||
const AuthenticatedApp = lazy(() =>
|
||||
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp')
|
||||
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp').then(
|
||||
(mod) => ({ default: mod.AuthenticatedApp })
|
||||
)
|
||||
);
|
||||
|
||||
function App() {
|
||||
|
@ -3,15 +3,11 @@ import * as React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { useNotification } from '../features/Notifications';
|
||||
import { useRBACProvider } from '../features/RBAC';
|
||||
import { Permission, useRBACProvider } from '../features/RBAC';
|
||||
import { hasPermissions } from '../utils/hasPermissions';
|
||||
|
||||
import { LoadingIndicatorPage } from './LoadingIndicatorPage';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
|
||||
export interface CheckPagePermissionsProps {
|
||||
children: React.ReactNode;
|
||||
permissions?: Permission[];
|
||||
|
@ -1,13 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useNotification } from '../features/Notifications';
|
||||
import { useRBACProvider } from '../features/RBAC';
|
||||
import { useRBACProvider, Permission } from '../features/RBAC';
|
||||
import { hasPermissions } from '../utils/hasPermissions';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
|
||||
// NOTE: this component is very similar to the CheckPagePermissions
|
||||
// except that it does not handle redirections nor loading state
|
||||
|
||||
|
@ -115,3 +115,5 @@ export {
|
||||
useAppInfo,
|
||||
useAppInfos,
|
||||
};
|
||||
|
||||
export type { AppInfoContextValue, AppInfoProviderProps };
|
||||
|
@ -128,3 +128,10 @@ const CustomFieldsProvider = ({ children, customFields }: CustomFieldsProviderPr
|
||||
const useCustomFields = () => React.useContext(CustomFieldsContext);
|
||||
|
||||
export { CustomFieldsContext, CustomFieldsProvider, useCustomFields };
|
||||
export type {
|
||||
CustomFieldsProviderProps,
|
||||
CustomField,
|
||||
CustomFieldComponents,
|
||||
CustomFieldOption,
|
||||
CustomFieldOptions,
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
* Context
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface LibraryContextValue {
|
||||
interface LibraryContextValue {
|
||||
fields?: Record<string, React.ComponentType>;
|
||||
components?: Record<string, React.ComponentType>;
|
||||
}
|
||||
@ -32,3 +32,4 @@ const LibraryProvider = ({ children, fields, components }: LibraryProviderProps)
|
||||
const useLibrary = () => React.useContext(LibraryContext);
|
||||
|
||||
export { LibraryContext, LibraryProvider, useLibrary };
|
||||
export type { LibraryContextValue, LibraryProviderProps };
|
||||
|
@ -1,9 +1,23 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
import type { Entity } from '@strapi/types';
|
||||
import type { QueryObserverBaseResult } from 'react-query';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
/**
|
||||
* This is duplicated from the `@strapi/admin` package.
|
||||
*/
|
||||
type Permission = {
|
||||
id?: Entity.ID;
|
||||
action: string;
|
||||
subject: string | null;
|
||||
actionParameters?: object;
|
||||
properties?: {
|
||||
fields?: string[];
|
||||
locales?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
conditions?: string[];
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Context
|
||||
|
@ -4,10 +4,7 @@ import { LinkProps } from 'react-router-dom';
|
||||
|
||||
import { TranslationMessage } from '../types';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
|
||||
import type { Permission } from './RBAC';
|
||||
interface MenuItem extends Pick<LinkProps, 'to'> {
|
||||
to: string;
|
||||
icon: React.ElementType;
|
||||
@ -62,7 +59,7 @@ type RunHookWaterfall = <InitialValue, Store>(
|
||||
store: Store
|
||||
) => unknown | Promise<unknown>;
|
||||
|
||||
export interface StrapiAppContextValue {
|
||||
interface StrapiAppContextValue {
|
||||
menu: MenuItem[];
|
||||
plugins: Record<string, Plugin>;
|
||||
settings: Record<string, StrapiAppSetting>;
|
||||
@ -124,3 +121,13 @@ const StrapiAppProvider = ({
|
||||
const useStrapiApp = () => React.useContext(StrapiAppContext);
|
||||
|
||||
export { StrapiAppContext, StrapiAppProvider, useStrapiApp };
|
||||
export type {
|
||||
StrapiAppProviderProps,
|
||||
StrapiAppContextValue,
|
||||
MenuItem,
|
||||
Plugin,
|
||||
StrapiAppSettingLink,
|
||||
StrapiAppSetting,
|
||||
RunHookSeries,
|
||||
RunHookWaterfall,
|
||||
};
|
||||
|
@ -2,15 +2,12 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useQueries } from 'react-query';
|
||||
|
||||
import { useRBACProvider } from '../features/RBAC';
|
||||
import { useRBACProvider, Permission } from '../features/RBAC';
|
||||
|
||||
import { useFetchClient } from './useFetchClient';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
|
||||
type AllowedActions = Record<string, boolean>;
|
||||
|
||||
export const useRBAC = (
|
||||
@ -71,8 +68,7 @@ export const useRBAC = (
|
||||
data: { data },
|
||||
} = await post<
|
||||
{ data: { data: boolean[] } },
|
||||
AxiosResponse<{ data: { data: boolean[] } }>,
|
||||
{ permissions: Permission[] }
|
||||
AxiosResponse<{ data: { data: boolean[] } }>
|
||||
>('/admin/permissions/check', {
|
||||
permissions: matchingPermissions.map(({ action, subject }) => ({
|
||||
action,
|
||||
|
@ -98,26 +98,16 @@ const auth = {
|
||||
sessionStorage.clear();
|
||||
},
|
||||
|
||||
get<T extends keyof StorageItems>(key: T): StorageItems[T] | string | null {
|
||||
const localStorageItem = localStorage.getItem(key);
|
||||
if (localStorageItem) {
|
||||
get<T extends keyof StorageItems>(key: T): StorageItems[T] | null {
|
||||
const item = localStorage.getItem(key) ?? sessionStorage.getItem(key);
|
||||
if (item) {
|
||||
try {
|
||||
const parsedItem = JSON.parse(localStorageItem);
|
||||
const parsedItem = JSON.parse(item);
|
||||
return parsedItem;
|
||||
} catch (error) {
|
||||
// Failed to parse return the string value
|
||||
return localStorageItem;
|
||||
}
|
||||
}
|
||||
|
||||
const sessionStorageItem = sessionStorage.getItem(key);
|
||||
if (sessionStorageItem) {
|
||||
try {
|
||||
const parsedItem = JSON.parse(sessionStorageItem);
|
||||
return parsedItem;
|
||||
} catch (error) {
|
||||
// Failed to parse return the string value
|
||||
return sessionStorageItem;
|
||||
// @ts-expect-error - this is fine
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { getFetchClient } from './getFetchClient';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
import type { Permission } from '../features/RBAC';
|
||||
import type { GenericAbortSignal } from 'axios';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
|
||||
const findMatchingPermissions = (userPermissions: Permission[], permissions: Permission[]) =>
|
||||
userPermissions.reduce<Permission[]>((acc, curr) => {
|
||||
const associatedPermission = permissions.find(
|
||||
@ -24,7 +22,7 @@ const formatPermissionsForRequest = (permissions: Permission[]) =>
|
||||
return {};
|
||||
}
|
||||
|
||||
const returnedPermission: Permission = {
|
||||
const returnedPermission: Partial<Permission> = {
|
||||
action: permission.action,
|
||||
};
|
||||
|
||||
|
@ -5,9 +5,7 @@ import {
|
||||
shouldCheckPermissions,
|
||||
} from '../hasPermissions';
|
||||
|
||||
import type { domain } from '@strapi/permissions';
|
||||
|
||||
type Permission = domain.permission.Permission;
|
||||
import type { Permission } from '../../features/RBAC';
|
||||
|
||||
const hasPermissionsTestData: Record<string, Record<string, Permission[]>> = {
|
||||
userPermissions: {
|
||||
|
@ -14,6 +14,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user