/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/core/content-manager/admin/src/layout.tsx
240 строк
8 KB
Arthur Moreau
fix: handle i18n conflict and local rights
03 авг 2026, 11:27
Не верифицирован
03 авг 2026, 11:27
6519f4d
Код
Авторство
О чём код?
/* eslint-disable check-file/filename-naming-convention */ import * as React from 'react'; import { Page, Layouts, SubNav, useIsMobile, LazyOutlet, useQueryParams, useNotification, useAuth, } from '@strapi/admin/strapi-admin'; import { stringify } from 'qs'; import { useIntl } from 'react-intl'; import { Navigate, useLocation, useMatch } from 'react-router-dom'; import { DragLayer, DragLayerProps } from './components/DragLayer'; import { CardDragPreview } from './components/DragPreviews/CardDragPreview'; import { ComponentDragPreview } from './components/DragPreviews/ComponentDragPreview'; import { RelationDragPreview } from './components/DragPreviews/RelationDragPreview'; import { LeftMenu } from './components/LeftMenu'; import { ItemTypes } from './constants/dragAndDrop'; import { useContentManagerInitData } from './hooks/useContentManagerInitData'; import { getTranslation } from './utils/translations'; import type { CardDragPreviewProps } from './components/DragPreviews/CardDragPreview'; import type { ComponentDragPreviewProps } from './components/DragPreviews/ComponentDragPreview'; import type { RelationDragPreviewProps } from './components/DragPreviews/RelationDragPreview'; import type { To } from 'react-router-dom'; /* ------------------------------------------------------------------------------------------------- * Layout * -----------------------------------------------------------------------------------------------*/ const Layout = () => { const contentTypeMatch = useMatch('/content-manager/:kind/:uid/*'); const isMobile = useIsMobile(); const { isLoading, collectionTypeLinks, models, singleTypeLinks } = useContentManagerInitData(); const authorisedModels = [...collectionTypeLinks, ...singleTypeLinks].sort((a, b) => a.title.localeCompare(b.title) ); const { pathname } = useLocation(); const { formatMessage } = useIntl(); const [{ query }] = useQueryParams<{ plugins?: { i18n?: { locale?: string } } }>(); const permissions = useAuth('Layout', (state) => state.permissions); if (isLoading) { return ( <> <Page.Title> {formatMessage({ id: getTranslation('plugin.name'), defaultMessage: 'Content Manager', })} </Page.Title> <Page.Loading /> </> ); } // Array of models that are displayed in the content manager const supportedModelsToDisplay = models.filter(({ isDisplayed }) => isDisplayed); // Redirect the user to the 403 page if ( authorisedModels.length === 0 && supportedModelsToDisplay.length > 0 && pathname !== '/content-manager/403' ) { const requestedLocale = query.plugins?.i18n?.locale; const localeScopedPermissions = permissions.filter( (permission) => permission.action === 'plugin::content-manager.explorer.read' && Array.isArray(permission.properties?.locales) ); const contentTypePermissions = localeScopedPermissions.filter( (permission) => permission.subject === contentTypeMatch?.params.uid ); const accessibleLocales = Array.from( new Set( (contentTypePermissions.length > 0 ? contentTypePermissions : localeScopedPermissions ).flatMap((permission) => permission.properties?.locales as string[]) ) ); if (typeof requestedLocale === 'string' && accessibleLocales.length > 0) { if (!accessibleLocales.includes(requestedLocale)) { const search = stringify({ ...query, plugins: { ...query.plugins, i18n: { ...query.plugins?.i18n, locale: accessibleLocales[0] }, }, }); return <NavigateWithLocaleWarning to={{ pathname, search }} />; } } else { return <Navigate to="/content-manager/403" replace />; } } // Redirect the user to the create content type page if (supportedModelsToDisplay.length === 0 && pathname !== '/content-manager/no-content-types') { return <Navigate to="/content-manager/no-content-types" replace />; } // On /content-manager base route if ( !contentTypeMatch && authorisedModels.length > 0 && pathname !== '/content-manager/403' && pathname !== '/content-manager/no-content-types' ) { // On desktop: redirect to first collection type if (!isMobile) { return ( <Navigate to={{ pathname: authorisedModels[0].to, search: authorisedModels[0].search ?? '', }} replace /> ); } // On mobile: show navigation page return ( <> <Page.Title> {formatMessage({ id: getTranslation('plugin.name'), defaultMessage: 'Content Manager', })} </Page.Title> <SubNav.PageWrapper> <LeftMenu isFullPage /> </SubNav.PageWrapper> </> ); } return ( <> <Page.Title> {formatMessage({ id: getTranslation('plugin.name'), defaultMessage: 'Content Manager', })} </Page.Title> <Layouts.Root sideNav={<LeftMenu />}> <DragLayer renderItem={renderDraglayerItem} /> <LazyOutlet nested /> </Layouts.Root> </> ); }; const NavigateWithLocaleWarning = ({ to }: { to: To }) => { const { toggleNotification } = useNotification(); const { formatMessage } = useIntl(); React.useEffect(() => { toggleNotification({ type: 'warning', message: formatMessage({ id: getTranslation('permissions.not-allowed.locale'), defaultMessage: "You don't have the permissions to access this content for the requested locale", }), }); }, [toggleNotification, formatMessage]); return <Navigate to={to} replace />; }; /* ------------------------------------------------------------------------------------------------- * renderDraglayerItem * -----------------------------------------------------------------------------------------------*/ function renderDraglayerItem({ type, item }: Parameters<DragLayerProps['renderItem']>[0]) { if (!type || (type && typeof type !== 'string')) { return null; } /** * Because a user may have multiple relations / dynamic zones / repeable fields in the same content type, * we append the fieldName for the item type to make them unique, however, we then want to extract that * first type to apply the correct preview. */ const [actualType] = type.split('_'); switch (actualType) { case ItemTypes.EDIT_FIELD: case ItemTypes.FIELD: return isCardDragItem(item) ? <CardDragPreview label={item.label} /> : null; case ItemTypes.COMPONENT: case ItemTypes.DYNAMIC_ZONE: return isComponentDragItem(item) ? ( <ComponentDragPreview displayedValue={item.displayedValue} /> ) : null; case ItemTypes.RELATION: return isRelationDragItem(item) ? <RelationDragPreview {...item} /> : null; default: return null; } } const isRecord = (value: unknown): value is Record<string, unknown> => { return value !== null && typeof value === 'object'; }; const isCardDragItem = (item: unknown): item is CardDragPreviewProps => { return isRecord(item) && typeof item.label === 'string'; }; const isComponentDragItem = (item: unknown): item is ComponentDragPreviewProps => { return isRecord(item) && typeof item.displayedValue === 'string'; }; const isRelationDragItem = (item: unknown): item is RelationDragPreviewProps => { return ( isRecord(item) && typeof item.displayedValue === 'string' && (typeof item.id === 'string' || typeof item.id === 'number') && typeof item.index === 'number' && typeof item.width === 'number' && (item.status === undefined || typeof item.status === 'string') ); }; export { Layout };