diff --git a/packages/core/admin/admin/src/App.tsx b/packages/core/admin/admin/src/App.tsx index bef822ffe6..8ee49dfe79 100644 --- a/packages/core/admin/admin/src/App.tsx +++ b/packages/core/admin/admin/src/App.tsx @@ -52,7 +52,7 @@ export const App = () => { defaultValue: ADMIN_PERMISSIONS_CE, } ); - const routes = useEnterprise( + const routes = useEnterprise( ROUTES_CE, async () => (await import('../../ee/admin/src/constants')).ROUTES_EE, { diff --git a/packages/core/admin/admin/src/constants.ts b/packages/core/admin/admin/src/constants.ts index 3fdecbf2ed..41db0dfa8a 100644 --- a/packages/core/admin/admin/src/constants.ts +++ b/packages/core/admin/admin/src/constants.ts @@ -1,3 +1,5 @@ +import { StrapiAppSettingLink } from '@strapi/helper-plugin'; + export const ADMIN_PERMISSIONS_CE = { contentManager: { 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_ADMIN_PERMISSIONS = 'StrapiAdmin/App/SET_ADMIN_PERMISSIONS'; -export const SETTINGS_LINKS_CE = () => ({ +export interface SettingsMenuLink extends Omit { + Component?: never; + lockIcon?: boolean; +} + +export type SettingsMenu = { + admin: SettingsMenuLink[]; + global: SettingsMenuLink[]; +}; + +export const SETTINGS_LINKS_CE = (): SettingsMenu => ({ global: [ { intlLabel: { id: 'Settings.application.title', defaultMessage: 'Overview' }, to: '/settings/application-infos', id: '000-application-infos', - permissions: [], }, { intlLabel: { id: 'Settings.webhooks.title', defaultMessage: 'Webhooks' }, @@ -182,7 +193,7 @@ export const SETTINGS_LINKS_CE = () => ({ id: 'roles', }, { - intlLabel: { id: 'global.users' }, + intlLabel: { id: 'global.users', defaultMessage: 'Users' }, // Init the search params directly to: '/settings/users?pageSize=10&page=1&sort=firstname', id: 'users', diff --git a/packages/core/admin/admin/src/hooks/__mocks__/useSettingsMenu.ts b/packages/core/admin/admin/src/hooks/__mocks__/useSettingsMenu.ts new file mode 100644 index 0000000000..90d377e490 --- /dev/null +++ b/packages/core/admin/admin/src/hooks/__mocks__/useSettingsMenu.ts @@ -0,0 +1,4 @@ +export const useSettingsMenu = jest.fn().mockReturnValue({ + isLoading: false, + menu: [], +}); diff --git a/packages/core/admin/admin/src/hooks/index.js b/packages/core/admin/admin/src/hooks/index.js deleted file mode 100644 index 48357c5362..0000000000 --- a/packages/core/admin/admin/src/hooks/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as useSettingsMenu } from './useSettingsMenu'; diff --git a/packages/core/admin/admin/src/hooks/tests/useEnterprise.test.ts b/packages/core/admin/admin/src/hooks/tests/useEnterprise.test.ts index e2660144e1..932732afb4 100644 --- a/packages/core/admin/admin/src/hooks/tests/useEnterprise.test.ts +++ b/packages/core/admin/admin/src/hooks/tests/useEnterprise.test.ts @@ -1,21 +1,15 @@ import { renderHook, waitFor } from '@testing-library/react'; -import { useEnterprise, UseEnterpriseOptions } from '../useEnterprise'; +import { useEnterprise } from '../useEnterprise'; const CE_DATA_FIXTURE = ['CE']; const EE_DATA_FIXTURE = ['EE']; -function setup( - ceData: any, - eeCallback: () => Promise, - options?: UseEnterpriseOptions -) { - return renderHook(() => useEnterprise(ceData, eeCallback, options)); -} - describe('useEnterprise (CE)', () => { 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); }); @@ -31,7 +25,9 @@ describe('useEnterprise (EE)', () => { }); 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); @@ -39,11 +35,13 @@ describe('useEnterprise (EE)', () => { }); test('Combines CE and EE data', async () => { - const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { - combine(ceData: string[], eeData: string[]) { - return [...ceData, ...eeData]; - }, - }); + const { result } = renderHook(() => + useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { + combine(ceData, eeData) { + return [...ceData, ...eeData]; + }, + }) + ); expect(result.current).toBe(null); @@ -53,23 +51,29 @@ describe('useEnterprise (EE)', () => { }); 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)); }); test('Returns CE data, when enabled is set to false', async () => { - const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { - enabled: false, - }); + const { result } = renderHook(() => + useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { + enabled: false, + }) + ); await waitFor(() => expect(result.current).toStrictEqual(CE_DATA_FIXTURE)); }); test('Returns a custom defaultValue on first render followed by the EE data', async () => { - const { result } = setup(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { - defaultValue: false, - }); + const { result } = renderHook(() => + useEnterprise(CE_DATA_FIXTURE, async () => EE_DATA_FIXTURE, { + defaultValue: false, + }) + ); expect(result.current).toBe(false); diff --git a/packages/core/admin/admin/src/hooks/useEnterprise.ts b/packages/core/admin/admin/src/hooks/useEnterprise.ts index 8d114d5b54..ae7d945aeb 100644 --- a/packages/core/admin/admin/src/hooks/useEnterprise.ts +++ b/packages/core/admin/admin/src/hooks/useEnterprise.ts @@ -2,50 +2,49 @@ import * as React from 'react'; 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() { return window.strapi.isEE; } -export interface UseEnterpriseOptions< - TCEData = unknown, - TEEData = unknown, - TCombinedData = TEEData -> { - defaultValue?: TCEData | TEEData | null; - combine?: (ceData: TCEData, eeData: TEEData) => TCombinedData; +export interface UseEnterpriseOptions { + defaultValue?: TDefaultValue; + combine?: (ceData: TCEData, eeData: TEEData) => TCombinedValue; enabled?: boolean; } -export function useEnterprise( +type UseEnterpriseReturn = + TDefaultValue extends null + ? TCEData | TEEData | TCombinedValue | null + : TCEData | TEEData | TCombinedValue | TDefaultValue; + +export const useEnterprise = < + TCEData, + TEEData = TCEData, + TCombinedValue = TEEData, + TDefaultValue = TCEData | null +>( ceData: TCEData, eeCallback: () => Promise, - { - defaultValue = null, - // @ts-expect-error – TODO: fix this type - combine = (ceData, eeData) => eeData, - enabled = true, - }: UseEnterpriseOptions = {} -): null | TCEData | TEEData | TCombinedData { + opts: UseEnterpriseOptions = {} +): UseEnterpriseReturn => { + const { defaultValue = null, combine = (_ceData, eeData) => eeData, enabled = true } = opts; const eeCallbackRef = useCallbackRef(eeCallback); const combineCallbackRef = useCallbackRef(combine); // We have to use a nested object here, because functions (e.g. Components) // 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, }); React.useEffect(() => { async function importEE() { const eeData = await eeCallbackRef(); + const combinedValue = combineCallbackRef(ceData, eeData); - setData({ data: combineCallbackRef(ceData, eeData) }); + setData({ data: combinedValue ? combinedValue : eeData }); } if (isEnterprise() && enabled) { @@ -53,5 +52,6 @@ export function useEnterprise + 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 { + links: Array; +} + +interface SettingsMenuLinkWithPermissionsAndDisplayed extends SettingsMenuLinkWithPermissions { + isDisplayed: boolean; +} + +interface StrapiAppSettingLinkWithDisplayed extends StrapiAppSettingLink { + isDisplayed: boolean; +} + +interface SettingsMenuSectionWithDisplayedLinks extends Omit { + links: Array; +} + +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[]>((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 }; diff --git a/packages/core/admin/admin/src/hooks/useSettingsMenu/index.js b/packages/core/admin/admin/src/hooks/useSettingsMenu/index.js deleted file mode 100644 index de8579624a..0000000000 --- a/packages/core/admin/admin/src/hooks/useSettingsMenu/index.js +++ /dev/null @@ -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; diff --git a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/formatLinks.js b/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/formatLinks.js deleted file mode 100644 index 515281c068..0000000000 --- a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/formatLinks.js +++ /dev/null @@ -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; diff --git a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/sortLinks.js b/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/sortLinks.js deleted file mode 100644 index 4d52b7539f..0000000000 --- a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/sortLinks.js +++ /dev/null @@ -1,5 +0,0 @@ -import sortBy from 'lodash/sortBy'; - -const sortLinks = (links) => sortBy(links, (link) => link.id); - -export default sortLinks; diff --git a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/tests/formatLinks.test.js b/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/tests/formatLinks.test.js deleted file mode 100644 index 16df23f4aa..0000000000 --- a/packages/core/admin/admin/src/hooks/useSettingsMenu/utils/tests/formatLinks.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/packages/core/admin/admin/src/pages/Auth/AuthPage.tsx b/packages/core/admin/admin/src/pages/Auth/AuthPage.tsx index 30b4d4728c..f3a83ad39b 100644 --- a/packages/core/admin/admin/src/pages/Auth/AuthPage.tsx +++ b/packages/core/admin/admin/src/pages/Auth/AuthPage.tsx @@ -24,7 +24,7 @@ const AuthPage = ({ hasAdmin }: AuthPageProps) => { LoginCE, async () => (await import('../../../../ee/admin/src/pages/AuthPage/components/Login')).LoginEE ); - const forms = useEnterprise, FormDictionary>( + const forms = useEnterprise>( FORMS, async () => (await import('../../../../ee/admin/src/pages/AuthPage/constants')).FORMS, { diff --git a/packages/core/admin/admin/src/pages/HomePage.tsx b/packages/core/admin/admin/src/pages/HomePage.tsx index 8733e741a2..0940b05db9 100644 --- a/packages/core/admin/admin/src/pages/HomePage.tsx +++ b/packages/core/admin/admin/src/pages/HomePage.tsx @@ -516,8 +516,7 @@ const SOCIAL_LINKS = [ * -----------------------------------------------------------------------------------------------*/ const HomePage = () => { - type CompFn = () => React.JSX.Element; - const Page = useEnterprise( + const Page = useEnterprise( HomePageCE, // eslint-disable-next-line import/no-cycle async () => (await import('../../../ee/admin/src/pages/HomePage')).HomePageEE diff --git a/packages/core/admin/admin/src/pages/SettingsPage/index.js b/packages/core/admin/admin/src/pages/SettingsPage/index.js index 1f99762521..42e1a8607a 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/index.js @@ -6,8 +6,8 @@ import { Helmet } from 'react-helmet'; import { useIntl } from 'react-intl'; import { Redirect, Route, Switch, useParams } from 'react-router-dom'; -import { useSettingsMenu } from '../../hooks'; import { useEnterprise } from '../../hooks/useEnterprise'; +import { useSettingsMenu } from '../../hooks/useSettingsMenu'; import { createRoute } from '../../utils/createRoute'; import SettingsNav from './components/SettingsNav'; diff --git a/packages/core/admin/admin/src/pages/SettingsPage/tests/index.test.js b/packages/core/admin/admin/src/pages/SettingsPage/tests/index.test.js index bc300c377f..74bf9f4c8d 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/tests/index.test.js @@ -10,13 +10,9 @@ import { Route, Router } from 'react-router-dom'; import { SettingsPage } from '..'; import { Theme } from '../../../components/Theme'; import { ThemeToggleProvider } from '../../../components/ThemeToggleProvider'; -import { useSettingsMenu } from '../../../hooks'; +import { useSettingsMenu } from '../../../hooks/useSettingsMenu'; -jest.mock('../../../hooks', () => ({ - useSettingsMenu: jest.fn(() => ({ isLoading: false, menu: [] })), - useAppInfo: jest.fn(() => ({ shouldUpdateStrapi: false })), - useThemeToggle: jest.fn(() => ({ currentTheme: 'light', themes: { light: lightTheme } })), -})); +jest.mock('../../../hooks/useSettingsMenu'); jest.mock('react-intl', () => ({ FormattedMessage: ({ id }) => id, diff --git a/packages/core/admin/admin/src/types/permissions.ts b/packages/core/admin/admin/src/types/permissions.ts index 2236c36c2d..b7829267e8 100644 --- a/packages/core/admin/admin/src/types/permissions.ts +++ b/packages/core/admin/admin/src/types/permissions.ts @@ -4,6 +4,19 @@ interface PermissionMap { marketplace: { 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 }; diff --git a/packages/core/admin/ee/admin/src/constants.ts b/packages/core/admin/ee/admin/src/constants.ts index 4289f05393..7e02124fc9 100644 --- a/packages/core/admin/ee/admin/src/constants.ts +++ b/packages/core/admin/ee/admin/src/constants.ts @@ -1,5 +1,7 @@ import { AuthResponse } from './pages/AuthResponse'; +import type { SettingsMenu } from '../../../admin/src/constants'; + export const ADMIN_PERMISSIONS_EE = { settings: { auditLogs: { @@ -31,7 +33,7 @@ export const ROUTES_EE = [ // 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 // which constant is available at which stage of the application lifecycle. -export const SETTINGS_LINKS_EE = () => ({ +export const SETTINGS_LINKS_EE = (): SettingsMenu => ({ global: [ ...(window.strapi.features.isEnabled(window.strapi.features.SSO) ? [