mirror of
https://github.com/strapi/strapi.git
synced 2025-11-13 08:38:09 +00:00
chore(admin): convert useSettingsMenu to TS (#18616)
* chore(admin): move constants into global constants * chore(admin): convert useSettingsMenu to TS
This commit is contained in:
parent
f5b09a8e61
commit
477ea8ee80
@ -52,7 +52,7 @@ export const App = () => {
|
|||||||
defaultValue: ADMIN_PERMISSIONS_CE,
|
defaultValue: ADMIN_PERMISSIONS_CE,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const routes = useEnterprise<StrapiRoute[] | null, StrapiRoute[], StrapiRoute[]>(
|
const routes = useEnterprise(
|
||||||
ROUTES_CE,
|
ROUTES_CE,
|
||||||
async () => (await import('../../ee/admin/src/constants')).ROUTES_EE,
|
async () => (await import('../../ee/admin/src/constants')).ROUTES_EE,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { StrapiAppSettingLink } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
export const ADMIN_PERMISSIONS_CE = {
|
export const ADMIN_PERMISSIONS_CE = {
|
||||||
contentManager: {
|
contentManager: {
|
||||||
main: [],
|
main: [],
|
||||||
@ -122,13 +124,22 @@ export const HOOKS = {
|
|||||||
export const ACTION_SET_APP_RUNTIME_STATUS = 'StrapiAdmin/APP/SET_APP_RUNTIME_STATUS';
|
export const ACTION_SET_APP_RUNTIME_STATUS = 'StrapiAdmin/APP/SET_APP_RUNTIME_STATUS';
|
||||||
export const ACTION_SET_ADMIN_PERMISSIONS = 'StrapiAdmin/App/SET_ADMIN_PERMISSIONS';
|
export const ACTION_SET_ADMIN_PERMISSIONS = 'StrapiAdmin/App/SET_ADMIN_PERMISSIONS';
|
||||||
|
|
||||||
export const SETTINGS_LINKS_CE = () => ({
|
export interface SettingsMenuLink extends Omit<StrapiAppSettingLink, 'Component' | 'permissions'> {
|
||||||
|
Component?: never;
|
||||||
|
lockIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SettingsMenu = {
|
||||||
|
admin: SettingsMenuLink[];
|
||||||
|
global: SettingsMenuLink[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SETTINGS_LINKS_CE = (): SettingsMenu => ({
|
||||||
global: [
|
global: [
|
||||||
{
|
{
|
||||||
intlLabel: { id: 'Settings.application.title', defaultMessage: 'Overview' },
|
intlLabel: { id: 'Settings.application.title', defaultMessage: 'Overview' },
|
||||||
to: '/settings/application-infos',
|
to: '/settings/application-infos',
|
||||||
id: '000-application-infos',
|
id: '000-application-infos',
|
||||||
permissions: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
intlLabel: { id: 'Settings.webhooks.title', defaultMessage: 'Webhooks' },
|
intlLabel: { id: 'Settings.webhooks.title', defaultMessage: 'Webhooks' },
|
||||||
@ -182,7 +193,7 @@ export const SETTINGS_LINKS_CE = () => ({
|
|||||||
id: 'roles',
|
id: 'roles',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
intlLabel: { id: 'global.users' },
|
intlLabel: { id: 'global.users', defaultMessage: 'Users' },
|
||||||
// Init the search params directly
|
// Init the search params directly
|
||||||
to: '/settings/users?pageSize=10&page=1&sort=firstname',
|
to: '/settings/users?pageSize=10&page=1&sort=firstname',
|
||||||
id: 'users',
|
id: 'users',
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export const useSettingsMenu = jest.fn().mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
menu: [],
|
||||||
|
});
|
||||||
@ -1 +0,0 @@
|
|||||||
export { default as useSettingsMenu } from './useSettingsMenu';
|
|
||||||
@ -1,21 +1,15 @@
|
|||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { useEnterprise, UseEnterpriseOptions } from '../useEnterprise';
|
import { useEnterprise } from '../useEnterprise';
|
||||||
|
|
||||||
const CE_DATA_FIXTURE = ['CE'];
|
const CE_DATA_FIXTURE = ['CE'];
|
||||||
const EE_DATA_FIXTURE = ['EE'];
|
const EE_DATA_FIXTURE = ['EE'];
|
||||||
|
|
||||||
function setup(
|
|
||||||
ceData: any,
|
|
||||||
eeCallback: () => Promise<any>,
|
|
||||||
options?: UseEnterpriseOptions<any, any, any>
|
|
||||||
) {
|
|
||||||
return renderHook(() => useEnterprise(ceData, eeCallback, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('useEnterprise (CE)', () => {
|
describe('useEnterprise (CE)', () => {
|
||||||
test('Returns CE data', async () => {
|
test('Returns CE data', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE);
|
const { result } = renderHook(() =>
|
||||||
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE)
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.current).toBe(CE_DATA_FIXTURE);
|
expect(result.current).toBe(CE_DATA_FIXTURE);
|
||||||
});
|
});
|
||||||
@ -31,7 +25,9 @@ describe('useEnterprise (EE)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Returns default data on first render and EE data on second', async () => {
|
test('Returns default data on first render and EE data on second', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE);
|
const { result } = renderHook(() =>
|
||||||
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE)
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.current).toBe(null);
|
expect(result.current).toBe(null);
|
||||||
|
|
||||||
@ -39,11 +35,13 @@ describe('useEnterprise (EE)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Combines CE and EE data', async () => {
|
test('Combines CE and EE data', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
const { result } = renderHook(() =>
|
||||||
combine(ceData: string[], eeData: string[]) {
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
||||||
return [...ceData, ...eeData];
|
combine(ceData, eeData) {
|
||||||
},
|
return [...ceData, ...eeData];
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.current).toBe(null);
|
expect(result.current).toBe(null);
|
||||||
|
|
||||||
@ -53,23 +51,29 @@ describe('useEnterprise (EE)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Returns EE data without custom combine', async () => {
|
test('Returns EE data without custom combine', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE);
|
const { result } = renderHook(() =>
|
||||||
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE)
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(result.current).toStrictEqual(EE_DATA_FIXTURE));
|
await waitFor(() => expect(result.current).toStrictEqual(EE_DATA_FIXTURE));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns CE data, when enabled is set to false', async () => {
|
test('Returns CE data, when enabled is set to false', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
const { result } = renderHook(() =>
|
||||||
enabled: false,
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
||||||
});
|
enabled: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(result.current).toStrictEqual(CE_DATA_FIXTURE));
|
await waitFor(() => expect(result.current).toStrictEqual(CE_DATA_FIXTURE));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns a custom defaultValue on first render followed by the EE data', async () => {
|
test('Returns a custom defaultValue on first render followed by the EE data', async () => {
|
||||||
const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
const { result } = renderHook(() =>
|
||||||
defaultValue: false,
|
useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, {
|
||||||
});
|
defaultValue: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.current).toBe(false);
|
expect(result.current).toBe(false);
|
||||||
|
|
||||||
|
|||||||
@ -2,50 +2,49 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { useCallbackRef } from '@strapi/helper-plugin';
|
import { useCallbackRef } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: this hook needs typing better, it's a bit similar to react-query's useQuery tbf
|
|
||||||
* We have an async function that returns something, and we can set initialData as well as
|
|
||||||
* a select function, the return type of the function should infer it all...
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isEnterprise() {
|
function isEnterprise() {
|
||||||
return window.strapi.isEE;
|
return window.strapi.isEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseEnterpriseOptions<
|
export interface UseEnterpriseOptions<TCEData, TEEData, TDefaultValue, TCombinedValue> {
|
||||||
TCEData = unknown,
|
defaultValue?: TDefaultValue;
|
||||||
TEEData = unknown,
|
combine?: (ceData: TCEData, eeData: TEEData) => TCombinedValue;
|
||||||
TCombinedData = TEEData
|
|
||||||
> {
|
|
||||||
defaultValue?: TCEData | TEEData | null;
|
|
||||||
combine?: (ceData: TCEData, eeData: TEEData) => TCombinedData;
|
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEnterprise<TCEData = unknown, TEEData = unknown, TCombinedData = TEEData>(
|
type UseEnterpriseReturn<TCEData, TEEData, TDefaultValue, TCombinedValue> =
|
||||||
|
TDefaultValue extends null
|
||||||
|
? TCEData | TEEData | TCombinedValue | null
|
||||||
|
: TCEData | TEEData | TCombinedValue | TDefaultValue;
|
||||||
|
|
||||||
|
export const useEnterprise = <
|
||||||
|
TCEData,
|
||||||
|
TEEData = TCEData,
|
||||||
|
TCombinedValue = TEEData,
|
||||||
|
TDefaultValue = TCEData | null
|
||||||
|
>(
|
||||||
ceData: TCEData,
|
ceData: TCEData,
|
||||||
eeCallback: () => Promise<TEEData>,
|
eeCallback: () => Promise<TEEData>,
|
||||||
{
|
opts: UseEnterpriseOptions<TCEData, TEEData, TDefaultValue, TCombinedValue> = {}
|
||||||
defaultValue = null,
|
): UseEnterpriseReturn<TCEData, TEEData, TDefaultValue, TCombinedValue> => {
|
||||||
// @ts-expect-error – TODO: fix this type
|
const { defaultValue = null, combine = (_ceData, eeData) => eeData, enabled = true } = opts;
|
||||||
combine = (ceData, eeData) => eeData,
|
|
||||||
enabled = true,
|
|
||||||
}: UseEnterpriseOptions<TCEData, TEEData, TCombinedData> = {}
|
|
||||||
): null | TCEData | TEEData | TCombinedData {
|
|
||||||
const eeCallbackRef = useCallbackRef(eeCallback);
|
const eeCallbackRef = useCallbackRef(eeCallback);
|
||||||
const combineCallbackRef = useCallbackRef(combine);
|
const combineCallbackRef = useCallbackRef(combine);
|
||||||
|
|
||||||
// We have to use a nested object here, because functions (e.g. Components)
|
// We have to use a nested object here, because functions (e.g. Components)
|
||||||
// can not be stored as value directly
|
// can not be stored as value directly
|
||||||
const [{ data }, setData] = React.useState<{ data: TCEData | TEEData | TCombinedData | null }>({
|
const [{ data }, setData] = React.useState<{
|
||||||
|
data: TCEData | TEEData | TDefaultValue | TCombinedValue | null;
|
||||||
|
}>({
|
||||||
data: isEnterprise() && enabled ? defaultValue : ceData,
|
data: isEnterprise() && enabled ? defaultValue : ceData,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function importEE() {
|
async function importEE() {
|
||||||
const eeData = await eeCallbackRef();
|
const eeData = await eeCallbackRef();
|
||||||
|
const combinedValue = combineCallbackRef(ceData, eeData);
|
||||||
|
|
||||||
setData({ data: combineCallbackRef(ceData, eeData) });
|
setData({ data: combinedValue ? combinedValue : eeData });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnterprise() && enabled) {
|
if (isEnterprise() && enabled) {
|
||||||
@ -53,5 +52,6 @@ export function useEnterprise<TCEData = unknown, TEEData = unknown, TCombinedDat
|
|||||||
}
|
}
|
||||||
}, [ceData, eeCallbackRef, combineCallbackRef, enabled]);
|
}, [ceData, eeCallbackRef, combineCallbackRef, enabled]);
|
||||||
|
|
||||||
|
// @ts-expect-error – the hook type assertion works in practice. But seems to have issues here...
|
||||||
return data;
|
return data;
|
||||||
}
|
};
|
||||||
|
|||||||
181
packages/core/admin/admin/src/hooks/useSettingsMenu.ts
Normal file
181
packages/core/admin/admin/src/hooks/useSettingsMenu.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasPermissions,
|
||||||
|
StrapiAppSetting,
|
||||||
|
StrapiAppSettingLink,
|
||||||
|
useRBACProvider,
|
||||||
|
useStrapiApp,
|
||||||
|
useAppInfo,
|
||||||
|
} from '@strapi/helper-plugin';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { SETTINGS_LINKS_CE, SettingsMenuLink } from '../constants';
|
||||||
|
import { selectAdminPermissions } from '../selectors';
|
||||||
|
|
||||||
|
import { useEnterprise } from './useEnterprise';
|
||||||
|
|
||||||
|
const formatLinks = (menu: SettingsMenuSection[]): SettingsMenuSectionWithDisplayedLinks[] =>
|
||||||
|
menu.map((menuSection) => {
|
||||||
|
const formattedLinks = menuSection.links.map((link) => ({
|
||||||
|
...link,
|
||||||
|
isDisplayed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { ...menuSection, links: formattedLinks };
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SettingsMenuLinkWithPermissions extends SettingsMenuLink {
|
||||||
|
permissions: StrapiAppSettingLink['permissions'];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsMenuSection extends Omit<StrapiAppSetting, 'links'> {
|
||||||
|
links: Array<SettingsMenuLinkWithPermissions | StrapiAppSettingLink>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsMenuLinkWithPermissionsAndDisplayed extends SettingsMenuLinkWithPermissions {
|
||||||
|
isDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StrapiAppSettingLinkWithDisplayed extends StrapiAppSettingLink {
|
||||||
|
isDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsMenuSectionWithDisplayedLinks extends Omit<SettingsMenuSection, 'links'> {
|
||||||
|
links: Array<SettingsMenuLinkWithPermissionsAndDisplayed | StrapiAppSettingLinkWithDisplayed>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsMenu = SettingsMenuSectionWithDisplayedLinks[];
|
||||||
|
|
||||||
|
const useSettingsMenu = (): {
|
||||||
|
isLoading: boolean;
|
||||||
|
menu: SettingsMenu;
|
||||||
|
} => {
|
||||||
|
const [{ isLoading, menu }, setData] = React.useState<{
|
||||||
|
isLoading: boolean;
|
||||||
|
menu: SettingsMenu;
|
||||||
|
}>({
|
||||||
|
isLoading: true,
|
||||||
|
menu: [],
|
||||||
|
});
|
||||||
|
const { allPermissions: userPermissions } = useRBACProvider();
|
||||||
|
const { shouldUpdateStrapi } = useAppInfo();
|
||||||
|
const { settings } = useStrapiApp();
|
||||||
|
const permissions = useSelector(selectAdminPermissions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* memoize the return value of this function to avoid re-computing it on every render
|
||||||
|
* because it's used in an effect it ends up re-running recursively.
|
||||||
|
*/
|
||||||
|
const ceLinks = React.useMemo(() => SETTINGS_LINKS_CE(), []);
|
||||||
|
|
||||||
|
const { admin: adminLinks, global: globalLinks } = useEnterprise(
|
||||||
|
ceLinks,
|
||||||
|
async () => (await import('../../../ee/admin/src/constants')).SETTINGS_LINKS_EE(),
|
||||||
|
{
|
||||||
|
combine(ceLinks, eeLinks) {
|
||||||
|
return {
|
||||||
|
admin: [...eeLinks.admin, ...ceLinks.admin],
|
||||||
|
global: [...ceLinks.global, ...eeLinks.global],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
admin: [],
|
||||||
|
global: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const addPermissions = React.useCallback(
|
||||||
|
(link: SettingsMenuLink) => {
|
||||||
|
if (!link.id) {
|
||||||
|
throw new Error('The settings menu item must have an id attribute.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...link,
|
||||||
|
permissions: permissions.settings?.[link.id]?.main ?? [],
|
||||||
|
} satisfies SettingsMenuLinkWithPermissions;
|
||||||
|
},
|
||||||
|
[permissions.settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const getData = async () => {
|
||||||
|
interface MenuLinkPermission {
|
||||||
|
hasPermission: boolean;
|
||||||
|
sectionIndex: number;
|
||||||
|
linkIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildMenuPermissions = (sections: SettingsMenuSectionWithDisplayedLinks[]) =>
|
||||||
|
Promise.all(
|
||||||
|
sections.reduce<Promise<MenuLinkPermission>[]>((acc, section, sectionIndex) => {
|
||||||
|
const linksWithPermissions = section.links.map(async (link, linkIndex) => ({
|
||||||
|
hasPermission: await hasPermissions(userPermissions, link.permissions),
|
||||||
|
sectionIndex,
|
||||||
|
linkIndex,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...acc, ...linksWithPermissions];
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const menuPermissions = await buildMenuPermissions(sections);
|
||||||
|
|
||||||
|
setData((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
isLoading: false,
|
||||||
|
menu: sections.map((section, sectionIndex) => ({
|
||||||
|
...section,
|
||||||
|
links: section.links.map((link, linkIndex) => {
|
||||||
|
const permission = menuPermissions.find(
|
||||||
|
(permission) =>
|
||||||
|
permission.sectionIndex === sectionIndex && permission.linkIndex === linkIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...link,
|
||||||
|
isDisplayed: Boolean(permission?.hasPermission),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { global, ...otherSections } = settings;
|
||||||
|
const sections = formatLinks([
|
||||||
|
{
|
||||||
|
...global,
|
||||||
|
links: sortBy([...global.links, ...globalLinks.map(addPermissions)], (link) => link.id).map(
|
||||||
|
(link) => ({
|
||||||
|
...link,
|
||||||
|
hasNotification: link.id === '000-application-infos' && shouldUpdateStrapi,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'permissions',
|
||||||
|
intlLabel: { id: 'Settings.permissions', defaultMessage: 'Administration Panel' },
|
||||||
|
links: adminLinks.map(addPermissions),
|
||||||
|
},
|
||||||
|
...Object.values(otherSections),
|
||||||
|
]);
|
||||||
|
|
||||||
|
getData();
|
||||||
|
}, [adminLinks, globalLinks, userPermissions, settings, shouldUpdateStrapi, addPermissions]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
menu: menu.map((menuItem) => ({
|
||||||
|
...menuItem,
|
||||||
|
links: menuItem.links.filter((link) => link.isDisplayed),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useSettingsMenu };
|
||||||
|
export type { SettingsMenu };
|
||||||
@ -1,133 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { hasPermissions, useRBACProvider, useStrapiApp, useAppInfo } from '@strapi/helper-plugin';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { SETTINGS_LINKS_CE } from '../../constants';
|
|
||||||
import { selectAdminPermissions } from '../../selectors';
|
|
||||||
import { useEnterprise } from '../useEnterprise';
|
|
||||||
|
|
||||||
import formatLinks from './utils/formatLinks';
|
|
||||||
import sortLinks from './utils/sortLinks';
|
|
||||||
|
|
||||||
const useSettingsMenu = () => {
|
|
||||||
const [{ isLoading, menu }, setData] = useState({
|
|
||||||
isLoading: true,
|
|
||||||
menu: [],
|
|
||||||
});
|
|
||||||
const { allPermissions: userPermissions } = useRBACProvider();
|
|
||||||
const { shouldUpdateStrapi } = useAppInfo();
|
|
||||||
const { settings } = useStrapiApp();
|
|
||||||
const permissions = useSelector(selectAdminPermissions);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* memoize the return value of this function to avoid re-computing it on every render
|
|
||||||
* because it's used in an effect it ends up re-running recursively.
|
|
||||||
*/
|
|
||||||
const ceLinks = useMemo(() => SETTINGS_LINKS_CE(), []);
|
|
||||||
|
|
||||||
const { global: globalLinks, admin: adminLinks } = useEnterprise(
|
|
||||||
ceLinks,
|
|
||||||
async () => (await import('../../../../ee/admin/src/constants')).SETTINGS_LINKS_EE(),
|
|
||||||
{
|
|
||||||
combine(ceLinks, eeLinks) {
|
|
||||||
return {
|
|
||||||
admin: [...eeLinks.admin, ...ceLinks.admin],
|
|
||||||
global: [...ceLinks.global, ...eeLinks.global],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
defaultValue: {
|
|
||||||
admin: [],
|
|
||||||
global: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const addPermissions = useCallback(
|
|
||||||
(link) => {
|
|
||||||
if (!link.id) {
|
|
||||||
throw new Error('The settings menu item must have an id attribute.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...link,
|
|
||||||
permissions: permissions.settings?.[link.id]?.main,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[permissions.settings]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getData = async () => {
|
|
||||||
const buildMenuPermissions = (sections) =>
|
|
||||||
Promise.all(
|
|
||||||
sections.reduce((acc, section, sectionIndex) => {
|
|
||||||
const buildMenuPermissions = (links) =>
|
|
||||||
links.map(async (link, linkIndex) => ({
|
|
||||||
hasPermission: await hasPermissions(userPermissions, link.permissions),
|
|
||||||
sectionIndex,
|
|
||||||
linkIndex,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...acc, ...buildMenuPermissions(section.links)];
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const menuPermissions = await buildMenuPermissions(sections);
|
|
||||||
|
|
||||||
setData((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
isLoading: false,
|
|
||||||
menu: sections.map((section, sectionIndex) => ({
|
|
||||||
...section,
|
|
||||||
links: section.links.map((link, linkIndex) => {
|
|
||||||
const permission = menuPermissions.find(
|
|
||||||
(permission) =>
|
|
||||||
permission.sectionIndex === sectionIndex && permission.linkIndex === linkIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...link,
|
|
||||||
isDisplayed: Boolean(permission.hasPermission),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { global, ...otherSections } = settings;
|
|
||||||
|
|
||||||
const sections = formatLinks([
|
|
||||||
{
|
|
||||||
...settings.global,
|
|
||||||
links: sortLinks([...settings.global.links, ...globalLinks.map(addPermissions)]).map(
|
|
||||||
(link) => ({
|
|
||||||
...link,
|
|
||||||
hasNotification: link.id === '000-application-infos' && shouldUpdateStrapi,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'permissions',
|
|
||||||
intlLabel: { id: 'Settings.permissions', defaultMessage: 'Administration Panel' },
|
|
||||||
links: adminLinks.map(addPermissions),
|
|
||||||
},
|
|
||||||
...Object.values(otherSections),
|
|
||||||
]);
|
|
||||||
|
|
||||||
getData();
|
|
||||||
}, [adminLinks, globalLinks, userPermissions, settings, shouldUpdateStrapi, addPermissions]);
|
|
||||||
|
|
||||||
const filterMenu = (menuItem) => {
|
|
||||||
return {
|
|
||||||
...menuItem,
|
|
||||||
links: menuItem.links.filter((link) => link.isDisplayed),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return { isLoading, menu: menu.map(filterMenu) };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSettingsMenu;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
const formatLinks = (menu) => {
|
|
||||||
return menu.map((menuSection) => {
|
|
||||||
const formattedLinks = menuSection.links.map((link) => ({
|
|
||||||
...link,
|
|
||||||
isDisplayed: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { ...menuSection, links: formattedLinks };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default formatLinks;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
|
|
||||||
const sortLinks = (links) => sortBy(links, (link) => link.id);
|
|
||||||
|
|
||||||
export default sortLinks;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import formatLinks from '../formatLinks';
|
|
||||||
|
|
||||||
describe('ADMIN | hooks | useSettingsMenu | utils | formatLinks', () => {
|
|
||||||
it('should add the isDisplayed key to all sections links', () => {
|
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
links: [{ name: 'link 1' }, { name: 'link 2' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
links: [{ name: 'link 3' }, { name: 'link 4' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
links: [
|
|
||||||
{ name: 'link 1', isDisplayed: false },
|
|
||||||
{ name: 'link 2', isDisplayed: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
links: [
|
|
||||||
{ name: 'link 3', isDisplayed: false },
|
|
||||||
{ name: 'link 4', isDisplayed: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(formatLinks(menu)).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -24,7 +24,7 @@ const AuthPage = ({ hasAdmin }: AuthPageProps) => {
|
|||||||
LoginCE,
|
LoginCE,
|
||||||
async () => (await import('../../../../ee/admin/src/pages/AuthPage/components/Login')).LoginEE
|
async () => (await import('../../../../ee/admin/src/pages/AuthPage/components/Login')).LoginEE
|
||||||
);
|
);
|
||||||
const forms = useEnterprise<FormDictionary, Partial<FormDictionary>, FormDictionary>(
|
const forms = useEnterprise<FormDictionary, Partial<FormDictionary>>(
|
||||||
FORMS,
|
FORMS,
|
||||||
async () => (await import('../../../../ee/admin/src/pages/AuthPage/constants')).FORMS,
|
async () => (await import('../../../../ee/admin/src/pages/AuthPage/constants')).FORMS,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -516,8 +516,7 @@ const SOCIAL_LINKS = [
|
|||||||
* -----------------------------------------------------------------------------------------------*/
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
type CompFn = () => React.JSX.Element;
|
const Page = useEnterprise(
|
||||||
const Page = useEnterprise<CompFn, CompFn, CompFn>(
|
|
||||||
HomePageCE,
|
HomePageCE,
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
async () => (await import('../../../ee/admin/src/pages/HomePage')).HomePageEE
|
async () => (await import('../../../ee/admin/src/pages/HomePage')).HomePageEE
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Redirect, Route, Switch, useParams } from 'react-router-dom';
|
import { Redirect, Route, Switch, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useSettingsMenu } from '../../hooks';
|
|
||||||
import { useEnterprise } from '../../hooks/useEnterprise';
|
import { useEnterprise } from '../../hooks/useEnterprise';
|
||||||
|
import { useSettingsMenu } from '../../hooks/useSettingsMenu';
|
||||||
import { createRoute } from '../../utils/createRoute';
|
import { createRoute } from '../../utils/createRoute';
|
||||||
|
|
||||||
import SettingsNav from './components/SettingsNav';
|
import SettingsNav from './components/SettingsNav';
|
||||||
|
|||||||
@ -10,13 +10,9 @@ import { Route, Router } from 'react-router-dom';
|
|||||||
import { SettingsPage } from '..';
|
import { SettingsPage } from '..';
|
||||||
import { Theme } from '../../../components/Theme';
|
import { Theme } from '../../../components/Theme';
|
||||||
import { ThemeToggleProvider } from '../../../components/ThemeToggleProvider';
|
import { ThemeToggleProvider } from '../../../components/ThemeToggleProvider';
|
||||||
import { useSettingsMenu } from '../../../hooks';
|
import { useSettingsMenu } from '../../../hooks/useSettingsMenu';
|
||||||
|
|
||||||
jest.mock('../../../hooks', () => ({
|
jest.mock('../../../hooks/useSettingsMenu');
|
||||||
useSettingsMenu: jest.fn(() => ({ isLoading: false, menu: [] })),
|
|
||||||
useAppInfo: jest.fn(() => ({ shouldUpdateStrapi: false })),
|
|
||||||
useThemeToggle: jest.fn(() => ({ currentTheme: 'light', themes: { light: lightTheme } })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('react-intl', () => ({
|
jest.mock('react-intl', () => ({
|
||||||
FormattedMessage: ({ id }) => id,
|
FormattedMessage: ({ id }) => id,
|
||||||
|
|||||||
@ -4,6 +4,19 @@ interface PermissionMap {
|
|||||||
marketplace: {
|
marketplace: {
|
||||||
main: Permission[];
|
main: Permission[];
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* TODO: remove the use of record to make it "concrete".
|
||||||
|
*/
|
||||||
|
settings: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
main: Permission[];
|
||||||
|
create: Permission[];
|
||||||
|
read: Permission[];
|
||||||
|
update: Permission[];
|
||||||
|
delete: Permission[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PermissionMap };
|
export { PermissionMap };
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { AuthResponse } from './pages/AuthResponse';
|
import { AuthResponse } from './pages/AuthResponse';
|
||||||
|
|
||||||
|
import type { SettingsMenu } from '../../../admin/src/constants';
|
||||||
|
|
||||||
export const ADMIN_PERMISSIONS_EE = {
|
export const ADMIN_PERMISSIONS_EE = {
|
||||||
settings: {
|
settings: {
|
||||||
auditLogs: {
|
auditLogs: {
|
||||||
@ -31,7 +33,7 @@ export const ROUTES_EE = [
|
|||||||
// TODO: the constants.js file is imported before the React application is setup and
|
// TODO: the constants.js file is imported before the React application is setup and
|
||||||
// therefore `window.strapi` might not exist at import-time. We should probably define
|
// therefore `window.strapi` might not exist at import-time. We should probably define
|
||||||
// which constant is available at which stage of the application lifecycle.
|
// which constant is available at which stage of the application lifecycle.
|
||||||
export const SETTINGS_LINKS_EE = () => ({
|
export const SETTINGS_LINKS_EE = (): SettingsMenu => ({
|
||||||
global: [
|
global: [
|
||||||
...(window.strapi.features.isEnabled(window.strapi.features.SSO)
|
...(window.strapi.features.isEnabled(window.strapi.features.SSO)
|
||||||
? [
|
? [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user