diff --git a/README.md b/README.md index 028336be..8cd160d5 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ Config for this plugin is stored as a part of the `config/plugins.{js|ts}` or `c - `pathDefaultFields` - The attribute to copy the default path from per content type. Syntax: `'api::.': ['url_slug', 'path']`. If you leave it empty, the default path is generated based on the ID. - `gql` - If you're using GraphQL that's the right place to put all necessary settings. More **[ here ](#gql-configuration)** - `cascadeMenuAttached` - If you don't want "Menu attached" to cascade on child items set this value `Disabled`. +- `isOverviewUiEnabled` - Enables the new management interface: an overview page listing all navigations with separate detail pages per navigation. ### Properties diff --git a/admin/src/api/validators.ts b/admin/src/api/validators.ts index 6a9c0d72..e99291d2 100644 --- a/admin/src/api/validators.ts +++ b/admin/src/api/validators.ts @@ -146,6 +146,7 @@ export const configSchema = z.object({ restrictedContentTypes: z.string().array(), isCacheEnabled: z.boolean().optional(), isCachePluginEnabled: z.boolean().optional(), + isOverviewUiEnabled: z.boolean().optional(), }); export type ConfigFromServerSchema = z.infer; diff --git a/admin/src/hooks/index.ts b/admin/src/hooks/index.ts new file mode 100644 index 00000000..b01e8b8e --- /dev/null +++ b/admin/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useAPI'; +export * from './useSettingsPermissions'; diff --git a/admin/src/pages/HomePage/hooks/useAPI.ts b/admin/src/hooks/useAPI.ts similarity index 97% rename from admin/src/pages/HomePage/hooks/useAPI.ts rename to admin/src/hooks/useAPI.ts index 8ef07de8..d64afdd5 100644 --- a/admin/src/pages/HomePage/hooks/useAPI.ts +++ b/admin/src/hooks/useAPI.ts @@ -2,9 +2,9 @@ import { getFetchClient } from '@strapi/strapi/admin'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback } from 'react'; -import { getApiClient } from '../../../api'; -import { NavigationSchema } from '../../../api/validators'; -import { Effect } from '../../../types'; +import { getApiClient } from '../api'; +import { NavigationSchema } from '../api/validators'; +import { Effect } from '../types'; import { appendViewId } from '../utils/appendViewId'; export const useLocale = () => { diff --git a/admin/src/pages/HomePage/hooks/useSettingsPermissions.ts b/admin/src/hooks/useSettingsPermissions.ts similarity index 89% rename from admin/src/pages/HomePage/hooks/useSettingsPermissions.ts rename to admin/src/hooks/useSettingsPermissions.ts index 38ec20ff..214bbba4 100644 --- a/admin/src/pages/HomePage/hooks/useSettingsPermissions.ts +++ b/admin/src/hooks/useSettingsPermissions.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useRBAC } from '@strapi/strapi/admin'; -import pluginPermissions from '../../../utils/permissions'; +import pluginPermissions from '../utils/permissions'; export const useSettingsPermissions = () => { const viewPermissions = useMemo( diff --git a/admin/src/pages/App.tsx b/admin/src/pages/App.tsx index 79994162..779eda5f 100644 --- a/admin/src/pages/App.tsx +++ b/admin/src/pages/App.tsx @@ -1,18 +1,48 @@ import { Page } from '@strapi/strapi/admin'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { Route, Routes } from 'react-router-dom'; +import { useConfig } from '../hooks'; +import { DetailPage } from './DetailPage'; import { HomePage } from './HomePage'; +import { OverviewPage } from './OverviewPage'; -const App = () => { - return ( - +const queryClient = new QueryClient(); + +const AppRoutes = () => { + const configQuery = useConfig(); + + if (configQuery.isPending) { + return ; + } + + if (configQuery.data?.isOverviewUiEnabled) { + return ( - + + - + ); + } + + return ( + + + + + ); +}; + +const App = () => { + return ( + + + + + ); }; diff --git a/admin/src/pages/HomePage/components/ChangeLanguageDialog/index.tsx b/admin/src/pages/DetailPage/components/ChangeLanguageDialog/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/ChangeLanguageDialog/index.tsx rename to admin/src/pages/DetailPage/components/ChangeLanguageDialog/index.tsx diff --git a/admin/src/pages/HomePage/components/CollapseButton/index.tsx b/admin/src/pages/DetailPage/components/CollapseButton/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/CollapseButton/index.tsx rename to admin/src/pages/DetailPage/components/CollapseButton/index.tsx diff --git a/admin/src/pages/HomePage/components/ControllableCombobox/index.tsx b/admin/src/pages/DetailPage/components/ControllableCombobox/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/ControllableCombobox/index.tsx rename to admin/src/pages/DetailPage/components/ControllableCombobox/index.tsx diff --git a/admin/src/pages/HomePage/components/DragButton/index.tsx b/admin/src/pages/DetailPage/components/DragButton/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/DragButton/index.tsx rename to admin/src/pages/DetailPage/components/DragButton/index.tsx diff --git a/admin/src/pages/HomePage/components/I18nCopyNavigationItems/index.tsx b/admin/src/pages/DetailPage/components/I18nCopyNavigationItems/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/I18nCopyNavigationItems/index.tsx rename to admin/src/pages/DetailPage/components/I18nCopyNavigationItems/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationContentHeader/ManageNavigationItems/index.tsx b/admin/src/pages/DetailPage/components/NavigationContentHeader/ManageNavigationItems/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationContentHeader/ManageNavigationItems/index.tsx rename to admin/src/pages/DetailPage/components/NavigationContentHeader/ManageNavigationItems/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationContentHeader/Search/index.tsx b/admin/src/pages/DetailPage/components/NavigationContentHeader/Search/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationContentHeader/Search/index.tsx rename to admin/src/pages/DetailPage/components/NavigationContentHeader/Search/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationContentHeader/index.tsx b/admin/src/pages/DetailPage/components/NavigationContentHeader/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationContentHeader/index.tsx rename to admin/src/pages/DetailPage/components/NavigationContentHeader/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationEmptyState/index.tsx b/admin/src/pages/DetailPage/components/NavigationEmptyState/index.tsx similarity index 98% rename from admin/src/pages/HomePage/components/NavigationEmptyState/index.tsx rename to admin/src/pages/DetailPage/components/NavigationEmptyState/index.tsx index f5de2ae9..9cd90423 100644 --- a/admin/src/pages/HomePage/components/NavigationEmptyState/index.tsx +++ b/admin/src/pages/DetailPage/components/NavigationEmptyState/index.tsx @@ -19,7 +19,7 @@ import { useResetNavigations, } from '../../hooks'; import { NavigationSchema } from '../../../../api/validators'; -import { appendViewId } from '../../utils/appendViewId'; +import { appendViewId } from '../../../../utils/appendViewId'; type NavigationEmptyStateProps = { canUpdate: boolean; diff --git a/admin/src/pages/DetailPage/components/NavigationHeader/index.tsx b/admin/src/pages/DetailPage/components/NavigationHeader/index.tsx new file mode 100644 index 00000000..b60791d7 --- /dev/null +++ b/admin/src/pages/DetailPage/components/NavigationHeader/index.tsx @@ -0,0 +1,120 @@ +import { Button, Flex, Link, SingleSelect, SingleSelectOption, Tag } from '@strapi/design-system'; +import { ArrowLeft, Check, Feather, Information } from '@strapi/icons'; +import { Layouts } from '@strapi/strapi/admin'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import { NavLink } from 'react-router-dom'; +import { NavigationSchema } from '../../../../api/validators'; +import { PLUGIN_ID } from '../../../../pluginId'; +import { getTrad } from '../../../../translations'; +import { Effect } from '../../../../types'; +import { useConfig, usePluginMediaQuery } from '../../hooks'; + +const submitIcon = ; + +interface Props { + activeNavigation?: NavigationSchema; + structureHasErrors?: boolean; + structureHasChanged?: boolean; + isSaving?: boolean; + handleLocalizationSelection: Effect; + handleSave: Effect; + handleCachePurge: Effect; + permissions: { canUpdate?: boolean }; + locale: { + defaultLocale: string; + restLocale: string[]; + }; + currentLocale?: string; +} + +export const NavigationHeader: React.FC = ({ + activeNavigation, + structureHasErrors, + structureHasChanged, + isSaving, + handleLocalizationSelection, + handleSave, + handleCachePurge, + permissions = {}, + locale, + currentLocale, +}) => { + const { formatMessage } = useIntl(); + + const configQuery = useConfig(); + + const hasLocalizations = !!locale.restLocale?.length; + const hasCache = !!configQuery.data?.isCacheEnabled; + + const { canUpdate } = permissions; + + const { isMobile } = usePluginMediaQuery(); + + return ( + } to={`/plugins/${PLUGIN_ID}`}> + {formatMessage(getTrad('popup.navigation.manage.button.goBack'))} + + } + title={activeNavigation?.name ?? formatMessage(getTrad('header.title', 'UI Navigation'))} + subtitle={formatMessage(getTrad('header.description'))} + primaryAction={ + + {hasLocalizations && ( + + {[locale.defaultLocale, ...locale.restLocale].map((code) => ( + + {code} + + ))} + + )} + {canUpdate && hasCache && ( + + )} + {canUpdate && ( + + )} + + } + secondaryAction={ + !isMobile && + activeNavigation && ( + }> + {formatMessage(getTrad('header.meta'), { + id: activeNavigation.documentId, + key: activeNavigation.slug, + })} + + ) + } + /> + ); +}; diff --git a/admin/src/pages/HomePage/components/NavigationHeader/styles.ts b/admin/src/pages/DetailPage/components/NavigationHeader/styles.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationHeader/styles.ts rename to admin/src/pages/DetailPage/components/NavigationHeader/styles.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/AdditionalFieldInput/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/AdditionalFieldInput/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/AdditionalFieldInput/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/AdditionalFieldInput/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/AudienceField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/AudienceField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/AudienceField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/AudienceField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/CustomFieldsField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/CustomFieldsField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/CustomFieldsField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/CustomFieldsField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/AdditionalFields/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/AttachToMenuField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/AttachToMenuField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/AttachToMenuField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/AttachToMenuField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/CopyFromLocales/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/CopyFromLocales/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/CopyFromLocales/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/CopyFromLocales/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/ManualPathField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/ManualPathField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/ManualPathField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/ManualPathField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/NavigationItemTypeField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/NavigationItemTypeField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/NavigationItemTypeField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/NavigationItemTypeField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/PathField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/PathField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/PathField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/PathField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/ReadFieldsFromRelatedField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/ReadFieldsFromRelatedField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/ReadFieldsFromRelatedField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/ReadFieldsFromRelatedField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedEntityField/hooks.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedEntityField/hooks.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedEntityField/hooks.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedEntityField/hooks.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedEntityField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedEntityField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedEntityField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedEntityField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedTypeField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedTypeField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/RelatedTypeField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/RelatedTypeField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/components/TitleField/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/components/TitleField/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/components/TitleField/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/components/TitleField/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/context/NavigationItemFormContext.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/context/NavigationItemFormContext.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/context/NavigationItemFormContext.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/context/NavigationItemFormContext.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemForm/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemForm/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/types.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/types.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/types.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/types.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/utils/form.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/utils/form.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/utils/form.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/utils/form.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/utils/hooks.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/utils/hooks.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemForm/utils/hooks.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/utils/hooks.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemForm/utils/properties.ts b/admin/src/pages/DetailPage/components/NavigationItemForm/utils/properties.ts similarity index 97% rename from admin/src/pages/HomePage/components/NavigationItemForm/utils/properties.ts rename to admin/src/pages/DetailPage/components/NavigationItemForm/utils/properties.ts index 2d1cc52e..97f9805f 100644 --- a/admin/src/pages/HomePage/components/NavigationItemForm/utils/properties.ts +++ b/admin/src/pages/DetailPage/components/NavigationItemForm/utils/properties.ts @@ -5,7 +5,7 @@ import { NavigationItemTypeSchema, StrapiContentTypeItemSchema, } from '../../../../../api/validators'; -import { extractRelatedItemLabel } from '../../../../HomePage/utils'; +import { extractRelatedItemLabel } from '../../../utils'; import { type NavigationItemFormSchema } from './form'; interface GenerateUiRouterKeyInput { diff --git a/admin/src/pages/HomePage/components/NavigationItemList/Wrapper.ts b/admin/src/pages/DetailPage/components/NavigationItemList/Wrapper.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemList/Wrapper.ts rename to admin/src/pages/DetailPage/components/NavigationItemList/Wrapper.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemList/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemList/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemList/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemList/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardBadge/index.ts b/admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardBadge/index.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardBadge/index.ts rename to admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardBadge/index.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/Wrapper.tsx b/admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/Wrapper.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/Wrapper.tsx rename to admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/Wrapper.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/icons.tsx b/admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/icons.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/icons.tsx rename to admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/icons.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardHeader/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardHeader/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardRemovedOverlay/index.ts b/admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardRemovedOverlay/index.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/ItemCardRemovedOverlay/index.ts rename to admin/src/pages/DetailPage/components/NavigationItemListItem/ItemCardRemovedOverlay/index.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/Wrapper.ts b/admin/src/pages/DetailPage/components/NavigationItemListItem/Wrapper.ts similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/Wrapper.ts rename to admin/src/pages/DetailPage/components/NavigationItemListItem/Wrapper.ts diff --git a/admin/src/pages/HomePage/components/NavigationItemListItem/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemListItem/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemListItem/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemListItem/index.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemPopup/NavigationItemPopupFooter.tsx b/admin/src/pages/DetailPage/components/NavigationItemPopup/NavigationItemPopupFooter.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemPopup/NavigationItemPopupFooter.tsx rename to admin/src/pages/DetailPage/components/NavigationItemPopup/NavigationItemPopupFooter.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemPopup/NavigationItemPopupHeader.tsx b/admin/src/pages/DetailPage/components/NavigationItemPopup/NavigationItemPopupHeader.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemPopup/NavigationItemPopupHeader.tsx rename to admin/src/pages/DetailPage/components/NavigationItemPopup/NavigationItemPopupHeader.tsx diff --git a/admin/src/pages/HomePage/components/NavigationItemPopup/index.tsx b/admin/src/pages/DetailPage/components/NavigationItemPopup/index.tsx similarity index 100% rename from admin/src/pages/HomePage/components/NavigationItemPopup/index.tsx rename to admin/src/pages/DetailPage/components/NavigationItemPopup/index.tsx diff --git a/admin/src/pages/DetailPage/components/index.ts b/admin/src/pages/DetailPage/components/index.ts new file mode 100644 index 00000000..e5022e8b --- /dev/null +++ b/admin/src/pages/DetailPage/components/index.ts @@ -0,0 +1 @@ +export * from './NavigationHeader'; diff --git a/admin/src/pages/HomePage/hooks/index.tsx b/admin/src/pages/DetailPage/hooks/index.tsx similarity index 77% rename from admin/src/pages/HomePage/hooks/index.tsx rename to admin/src/pages/DetailPage/hooks/index.tsx index 89e9b419..7c054dce 100644 --- a/admin/src/pages/HomePage/hooks/index.tsx +++ b/admin/src/pages/DetailPage/hooks/index.tsx @@ -1,8 +1,7 @@ -export * from './useAPI'; +export * from '../../../hooks'; export * from './useI18nCopyNavigationItemsModal'; export * from './useInvalidateQueries'; export * from './useLocales'; export * from './useNavigationItemPopup'; export * from './useSearch'; -export * from './useSettingsPermissions'; export * from './usePluginMediaQuery'; diff --git a/admin/src/pages/HomePage/hooks/useI18nCopyNavigationItemsModal.tsx b/admin/src/pages/DetailPage/hooks/useI18nCopyNavigationItemsModal.tsx similarity index 100% rename from admin/src/pages/HomePage/hooks/useI18nCopyNavigationItemsModal.tsx rename to admin/src/pages/DetailPage/hooks/useI18nCopyNavigationItemsModal.tsx diff --git a/admin/src/pages/HomePage/hooks/useInvalidateQueries.ts b/admin/src/pages/DetailPage/hooks/useInvalidateQueries.ts similarity index 100% rename from admin/src/pages/HomePage/hooks/useInvalidateQueries.ts rename to admin/src/pages/DetailPage/hooks/useInvalidateQueries.ts diff --git a/admin/src/pages/HomePage/hooks/useLocales.ts b/admin/src/pages/DetailPage/hooks/useLocales.ts similarity index 100% rename from admin/src/pages/HomePage/hooks/useLocales.ts rename to admin/src/pages/DetailPage/hooks/useLocales.ts diff --git a/admin/src/pages/HomePage/hooks/useNavigationItemPopup.ts b/admin/src/pages/DetailPage/hooks/useNavigationItemPopup.ts similarity index 100% rename from admin/src/pages/HomePage/hooks/useNavigationItemPopup.ts rename to admin/src/pages/DetailPage/hooks/useNavigationItemPopup.ts diff --git a/admin/src/pages/HomePage/hooks/usePluginMediaQuery.tsx b/admin/src/pages/DetailPage/hooks/usePluginMediaQuery.tsx similarity index 100% rename from admin/src/pages/HomePage/hooks/usePluginMediaQuery.tsx rename to admin/src/pages/DetailPage/hooks/usePluginMediaQuery.tsx diff --git a/admin/src/pages/HomePage/hooks/useSearch.ts b/admin/src/pages/DetailPage/hooks/useSearch.ts similarity index 100% rename from admin/src/pages/HomePage/hooks/useSearch.ts rename to admin/src/pages/DetailPage/hooks/useSearch.ts diff --git a/admin/src/pages/DetailPage/index.tsx b/admin/src/pages/DetailPage/index.tsx new file mode 100644 index 00000000..ba5b9c77 --- /dev/null +++ b/admin/src/pages/DetailPage/index.tsx @@ -0,0 +1,256 @@ +import { Layouts, Page, useNotification } from '@strapi/strapi/admin'; +import { SyntheticEvent, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { NavigationSchema } from '../../api/validators'; +import { PLUGIN_ID } from '../../pluginId'; +import { getTrad } from '../../translations'; +import { appendViewId } from '../../utils/appendViewId'; +import { NavigationHeader } from './components'; +import { ChangeLanguageDialog } from './components/ChangeLanguageDialog'; +import { NavigationContentHeader } from './components/NavigationContentHeader'; +import { List } from './components/NavigationItemList'; +import NavigationItemPopUp from './components/NavigationItemPopup'; +import { Search } from './components/NavigationContentHeader/Search'; +import { + useConfig, + useInvalidateQueries, + useLocales, + useNavigationItemPopup, + useNavigations, + usePurgeNavigation, + useSearch, + useSettingsPermissions, + useUpdateNavigation, +} from './hooks'; +import { getPendingAction, transformItemToViewPayload } from './utils'; +import { ManageNavigationItems } from './components/NavigationContentHeader/ManageNavigationItems'; +import { NavigationEmptyState } from './components/NavigationEmptyState'; +import { type NavigationItemFormSchema } from './components/NavigationItemForm'; + +const DetailPage = () => { + const { formatMessage } = useIntl(); + const { documentId } = useParams<{ documentId: string }>(); + const navigate = useNavigate(); + + const navigationsQuery = useNavigations(); + const configQuery = useConfig(); + const purgeMutation = usePurgeNavigation(); + + const { toggleNotification } = useNotification(); + + const [currentNavigation, setCurrentNavigation] = useState(); + const [structureChanged, setStructureChanged] = useState(false); + + const { canAccess, canUpdate, isLoadingForPermissions } = useSettingsPermissions(); + + const { + localeData, + currentLocale, + isChangeLanguageVisible, + changeCurrentLocaleAction, + availableLocales, + } = useLocales(navigationsQuery.data, setStructureChanged); + + const { searchValue, setSearchValue, isSearchEmpty, filteredList } = useSearch(currentNavigation); + + const { + activeNavigationItem, + addNewNavigationItem, + editNavigationItem, + closeNavigationItemPopup, + isItemPopupVisible, + } = useNavigationItemPopup(canUpdate); + + const pending = getPendingAction([navigationsQuery, { isPending: isLoadingForPermissions }]); + + const updateNavigationMutation = useUpdateNavigation({ + onError: (error: any) => { + toggleNotification({ + type: 'danger', + message: formatMessage(getTrad('notification.navigation.update.error')), + }); + + try { + console.error(error); + console.log(error.response.data.error); + } catch (e) {} + }, + onSuccess: (next) => { + setCurrentNavigation({ + ...next, + items: next.items.map(appendViewId), + }); + + setStructureChanged(false); + }, + }); + + const submit = () => { + if (currentNavigation) { + updateNavigationMutation.mutate(currentNavigation); + } + }; + + const onPopUpClose = (e: SyntheticEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Using Strapi design system select components inside a modal causes + // extraneous close events to be fired. It is most likely related to how + // the select component handles onOutsideClick events. In those situations + // the event target element is the root HTML element. + // This is a workaround to prevent the modal from closing in those cases. + if ((e.target as any).tagName !== 'HTML') { + closeNavigationItemPopup(); + } + }; + + const handleSubmitNavigationItem = (payload: NavigationItemFormSchema) => { + if (currentNavigation && configQuery.data) { + const items = transformItemToViewPayload( + payload, + currentNavigation?.items ?? [], + configQuery.data + ); + + setCurrentNavigation({ + ...currentNavigation, + items, + }); + + setStructureChanged(true); + + closeNavigationItemPopup(); + } + }; + + const listItems = isSearchEmpty ? (currentNavigation?.items ?? []) : filteredList; + + useEffect(() => { + if (!navigationsQuery.data) { + return; + } + + const localeVersions = navigationsQuery.data.filter( + (navigation) => navigation.documentId === documentId + ); + + if (!localeVersions.length) { + navigate(`/plugins/${PLUGIN_ID}`); + return; + } + + if (!currentNavigation || currentNavigation.documentId !== documentId) { + setCurrentNavigation( + localeVersions.find((navigation) => navigation.locale === currentLocale) ?? + localeVersions[0] + ); + } + }, [navigationsQuery.data, currentNavigation, documentId, currentLocale, navigate]); + + useEffect(() => { + if (currentNavigation && currentLocale !== currentNavigation.locale) { + const nextNavigation = navigationsQuery.data?.find( + (navigation) => + navigation.documentId === currentNavigation.documentId && + navigation.locale === currentLocale + ); + + if ( + nextNavigation && + nextNavigation.documentId === currentNavigation.documentId && + nextNavigation.locale !== currentNavigation.locale + ) { + setCurrentNavigation(nextNavigation); + } + } + }, [currentNavigation, currentLocale, navigationsQuery.data]); + + useInvalidateQueries(); + + if (!navigationsQuery.data || !localeData || !!pending) { + return ; + } + + return ( + + + + purgeMutation.mutate(documentId ? [documentId] : undefined)} + handleLocalizationSelection={ + structureChanged ? changeCurrentLocaleAction.trigger : changeCurrentLocaleAction.perform + } + handleSave={submit} + locale={localeData} + structureHasChanged={structureChanged} + isSaving={updateNavigationMutation.isPending} + permissions={{ canUpdate }} + currentLocale={currentLocale} + /> + + + } + endActions={ + + } + /> + {!currentNavigation?.items.length ? ( + + ) : ( + + )} + + {isItemPopupVisible && currentLocale && currentNavigation && ( + + )} + + {isChangeLanguageVisible && ( + changeCurrentLocaleAction.cancel()} + onConfirm={() => changeCurrentLocaleAction.perform()} + /> + )} + + + + ); +}; + +export { DetailPage }; diff --git a/admin/src/pages/HomePage/utils/index.ts b/admin/src/pages/DetailPage/utils/index.ts similarity index 100% rename from admin/src/pages/HomePage/utils/index.ts rename to admin/src/pages/DetailPage/utils/index.ts diff --git a/admin/src/pages/HomePage/utils/parsers.ts b/admin/src/pages/DetailPage/utils/parsers.ts similarity index 100% rename from admin/src/pages/HomePage/utils/parsers.ts rename to admin/src/pages/DetailPage/utils/parsers.ts diff --git a/admin/src/pages/HomePage/components/NavigationHeader/index.tsx b/admin/src/pages/HomePage/components/NavigationHeader/index.tsx index 0623aaf0..a332a00a 100644 --- a/admin/src/pages/HomePage/components/NavigationHeader/index.tsx +++ b/admin/src/pages/HomePage/components/NavigationHeader/index.tsx @@ -16,7 +16,7 @@ import styled from 'styled-components'; import { NavigationSchema } from '../../../../api/validators'; import { getTrad } from '../../../../translations'; import { Effect } from '../../../../types'; -import { useConfig, usePluginMediaQuery } from '../../hooks'; +import { useConfig, usePluginMediaQuery } from '../../../DetailPage/hooks'; import { useNavigationManager } from './hooks'; const StyledGridItem = styled(Grid.Item)<{ diff --git a/admin/src/pages/HomePage/components/NavigationManager/AllNavigations/index.tsx b/admin/src/pages/HomePage/components/NavigationManager/AllNavigations/index.tsx index b2d717a9..4e68c162 100644 --- a/admin/src/pages/HomePage/components/NavigationManager/AllNavigations/index.tsx +++ b/admin/src/pages/HomePage/components/NavigationManager/AllNavigations/index.tsx @@ -18,7 +18,7 @@ import { useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { getTrad } from '../../../../../translations'; -import { useConfig, useLocale } from '../../../hooks'; +import { useConfig, useLocale } from '../../../../DetailPage/hooks'; import { Footer, FooterBase } from '../Footer'; import { INITIAL_NAVIGATION } from '../NewNavigation'; import { CommonProps, ListState, Navigation } from '../types'; diff --git a/admin/src/pages/HomePage/components/NavigationManager/index.tsx b/admin/src/pages/HomePage/components/NavigationManager/index.tsx index c895c936..c3a4cb8a 100644 --- a/admin/src/pages/HomePage/components/NavigationManager/index.tsx +++ b/admin/src/pages/HomePage/components/NavigationManager/index.tsx @@ -13,8 +13,8 @@ import { useNavigations, usePurgeNavigation, useUpdateNavigation, -} from '../../hooks'; -import { getPendingAction } from '../../utils'; +} from '../../../DetailPage/hooks'; +import { getPendingAction } from '../../../DetailPage/utils'; import { AllNavigations, AllNavigationsFooter } from './AllNavigations'; import { DeleteConfirmFooter, DeletionConfirm } from './DeletionConfirm'; import { ErrorDetails, ErrorDetailsFooter } from './ErrorDetails'; diff --git a/admin/src/pages/HomePage/index.tsx b/admin/src/pages/HomePage/index.tsx index 6df2148f..c60204bc 100644 --- a/admin/src/pages/HomePage/index.tsx +++ b/admin/src/pages/HomePage/index.tsx @@ -8,11 +8,11 @@ import { useIntl } from 'react-intl'; import { NavigationSchema } from '../../api/validators'; import { getTrad } from '../../translations'; import { NavigationHeader } from './components'; -import { ChangeLanguageDialog } from './components/ChangeLanguageDialog'; -import { NavigationContentHeader } from './components/NavigationContentHeader'; -import { List } from './components/NavigationItemList'; -import NavigationItemPopUp from './components/NavigationItemPopup'; -import { Search } from './components/NavigationContentHeader/Search'; +import { ChangeLanguageDialog } from '../DetailPage/components/ChangeLanguageDialog'; +import { NavigationContentHeader } from '../DetailPage/components/NavigationContentHeader'; +import { List } from '../DetailPage/components/NavigationItemList'; +import NavigationItemPopUp from '../DetailPage/components/NavigationItemPopup'; +import { Search } from '../DetailPage/components/NavigationContentHeader/Search'; import { useConfig, useInvalidateQueries, @@ -23,12 +23,12 @@ import { useSearch, useSettingsPermissions, useUpdateNavigation, -} from './hooks'; -import { getPendingAction, transformItemToViewPayload } from './utils'; -import { ManageNavigationItems } from './components/NavigationContentHeader/ManageNavigationItems'; -import { NavigationEmptyState } from './components/NavigationEmptyState'; -import { appendViewId } from './utils/appendViewId'; -import { type NavigationItemFormSchema } from './components/NavigationItemForm'; +} from '../DetailPage/hooks'; +import { getPendingAction, transformItemToViewPayload } from '../DetailPage/utils'; +import { ManageNavigationItems } from '../DetailPage/components/NavigationContentHeader/ManageNavigationItems'; +import { NavigationEmptyState } from '../DetailPage/components/NavigationEmptyState'; +import { appendViewId } from '../../utils/appendViewId'; +import { type NavigationItemFormSchema } from '../DetailPage/components/NavigationItemForm'; const queryClient = new QueryClient(); diff --git a/admin/src/pages/OverviewPage/components/NavigationForm/hooks.ts b/admin/src/pages/OverviewPage/components/NavigationForm/hooks.ts new file mode 100644 index 00000000..52c4af44 --- /dev/null +++ b/admin/src/pages/OverviewPage/components/NavigationForm/hooks.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const formSchema = ({ alreadyUsedNames }: { alreadyUsedNames: string[] }) => + z.object({ + name: z + .string() + .min(2) // TODO: add translation + .and(z.string().refine((name) => !alreadyUsedNames.includes(name), 'Name already used')), // TODO: add translation + visible: z.boolean(), + }); diff --git a/admin/src/pages/OverviewPage/components/NavigationForm/index.tsx b/admin/src/pages/OverviewPage/components/NavigationForm/index.tsx new file mode 100644 index 00000000..69f70a3f --- /dev/null +++ b/admin/src/pages/OverviewPage/components/NavigationForm/index.tsx @@ -0,0 +1,175 @@ +import { Grid, TextInput, Toggle } from '@strapi/design-system'; +import { Form as StrapiForm } from '@strapi/strapi/admin'; +import { get, isEmpty, isNil, isObject, isString, set } from 'lodash'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Field } from '@sensinum/strapi-utils'; + +import { getTrad } from '../../../../translations'; +import { Effect, FormChangeEvent, FormItemErrorSchema } from '../../../../types'; +import { Navigation } from '../../types'; +import { formSchema } from './hooks'; + +interface Props> { + navigation: T; + onChange: Effect; + isLoading?: boolean; + alreadyUsedNames?: Array; +} + +export const Form = >({ + navigation, + onChange, + alreadyUsedNames = [], + isLoading, +}: Props) => { + const [formValue, setFormValue] = useState({} as T); + const [formError, setFormError] = useState>(); + + const { formatMessage } = useIntl(); + + const { name, visible } = formValue; + + const handleChange = ( + eventOrPath: FormChangeEvent, + value?: any, + nativeOnChange?: (eventOrPath: FormChangeEvent, value?: any) => void + ) => { + if (nativeOnChange) { + let fieldName = eventOrPath; + let fieldValue = value; + + if (isObject(eventOrPath)) { + const { name: targetName, value: targetValue } = eventOrPath.target; + fieldName = targetName; + fieldValue = isNil(fieldValue) ? targetValue : fieldValue; + } + + if (isString(fieldName)) { + setFormValueItem(fieldName, fieldValue); + } + + return nativeOnChange(eventOrPath as FormChangeEvent, fieldValue); + } + }; + + const setFormValueItem = (path: string, value: any) => { + setFormValue( + set( + { + ...formValue, + }, + path, + value + ) + ); + }; + + const renderError = (error: string): string | undefined => { + const errorOccurence = get(formError, error); + if (errorOccurence) { + return errorOccurence; + } + return undefined; + }; + + useEffect(() => { + if (navigation) { + if (navigation.name) { + setFormValue({ + ...navigation, + } as T); + } else { + setFormValue({ + name: 'New navigation', + visible: true, + } as T); + + onChange({ + name: 'New navigation', + visible: true, + disabled: true, + } as unknown as T); + } + } + }, []); + + useEffect(() => { + if (name === undefined && visible === undefined) { + return; + } + + if (`${name}-${visible}` !== `${navigation.name}-${navigation.visible}`) { + const { error } = formSchema({ alreadyUsedNames }).safeParse(formValue); + + onChange({ + ...navigation, + name, + visible, + disabled: !isEmpty(error?.issues), + }); + if (error) { + setFormError( + error.issues.reduce((acc, err) => { + return { + ...acc, + [err.path.join('.')]: err.message, + }; + }, {} as FormItemErrorSchema) + ); + } else { + setFormError(undefined); + } + } + }, [name, visible]); + + return ( + + {({ values, onChange }) => { + return ( + + + + + handleChange(eventOrPath, value, onChange) + } + value={values.name} + disabled={isLoading} + /> + + + + + + handleChange(eventOrPath, !values.visible, onChange) + } + onLabel={formatMessage(getTrad('popup.navigation.form.visible.toggle.visible'))} + offLabel={formatMessage(getTrad('popup.navigation.form.visible.toggle.hidden'))} + disabled={isLoading} + width="100%" + /> + + + + ); + }} + + ); +}; diff --git a/admin/src/pages/OverviewPage/components/NavigationFormModal/index.tsx b/admin/src/pages/OverviewPage/components/NavigationFormModal/index.tsx new file mode 100644 index 00000000..395407db --- /dev/null +++ b/admin/src/pages/OverviewPage/components/NavigationFormModal/index.tsx @@ -0,0 +1,77 @@ +import { Button, Modal } from '@strapi/design-system'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; + +import { getTrad } from '../../../../translations'; +import { Effect, VoidEffect } from '../../../../types'; +import { Navigation } from '../../types'; +import { Form } from '../NavigationForm'; + +interface Props { + navigation: Partial; + alreadyUsedNames: Array; + isLoading?: boolean; + onClose: VoidEffect; + onSubmit: Effect>; +} + +export const NavigationFormModal = ({ + navigation, + alreadyUsedNames, + isLoading, + onClose, + onSubmit, +}: Props) => { + const { formatMessage } = useIntl(); + + const [current, setCurrent] = useState(navigation); + const [isDisabled, setIsDisabled] = useState(false); + + const isEdit = !!navigation.documentId; + + const handleChange = ({ disabled, ...updated }: Partial & { disabled?: boolean }) => { + setCurrent(updated); + setIsDisabled(!!disabled); + }; + + return ( + { + if (!isOpen) { + onClose(); + } + }} + > + + + + {isEdit + ? formatMessage(getTrad('popup.navigation.manage.header.EDIT'), { + name: navigation.name, + }) + : formatMessage(getTrad('popup.navigation.manage.header.CREATE'))} + + + +
+ + + + + + + + + + ); +}; diff --git a/admin/src/pages/OverviewPage/index.tsx b/admin/src/pages/OverviewPage/index.tsx new file mode 100644 index 00000000..94b90b12 --- /dev/null +++ b/admin/src/pages/OverviewPage/index.tsx @@ -0,0 +1,365 @@ +import { + Button, + EmptyStateLayout, + Flex, + IconButton, + Status, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + Typography, + VisuallyHidden, +} from '@strapi/design-system'; +import { Feather, Pencil, Plus, Trash } from '@strapi/icons'; +import { EmptyDocuments } from '@strapi/icons/symbols'; +import { Layouts, Page, useNotification } from '@strapi/strapi/admin'; +import { sortBy } from 'lodash'; +import { useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useNavigate } from 'react-router-dom'; + +import { ConfirmationDialog } from '../../components/ConfirmationDialog'; +import { + useConfig, + useCreateNavigation, + useDeleteNavigations, + useHardReset, + useLocale, + useNavigations, + usePurgeNavigation, + useSettingsPermissions, + useUpdateNavigation, +} from '../../hooks'; +import { PLUGIN_ID } from '../../pluginId'; +import { getTrad } from '../../translations'; +import { NavigationFormModal } from './components/NavigationFormModal'; +import { Navigation } from './types'; + +const INITIAL_NAVIGATION: Partial = { + name: '', + items: [], + visible: false, +}; + +type OverlayState = + | { type: 'create' } + | { type: 'edit'; navigation: Navigation } + | { type: 'delete'; navigation: Navigation } + | { type: 'purge'; navigation?: Navigation } + | null; + +const OverviewPage = () => { + const { formatMessage } = useIntl(); + const navigate = useNavigate(); + const { toggleNotification } = useNotification(); + + const navigationsQuery = useNavigations(); + const localeQuery = useLocale(); + const configQuery = useConfig(); + const { canUpdate, isLoadingForPermissions } = useSettingsPermissions(); + + const createNavigationMutation = useCreateNavigation(); + const updateNavigationMutation = useUpdateNavigation({}); + const deleteNavigationsMutation = useDeleteNavigations(); + const purgeNavigationsMutation = usePurgeNavigation(); + const hardReset = useHardReset(); + + const [overlay, setOverlay] = useState(null); + + const navigations = useMemo( + () => sortBy(navigationsQuery.data ?? [], 'id'), + [navigationsQuery.data] + ); + + const defaultLocaleNavigations = useMemo( + () => navigations.filter(({ locale }) => locale === localeQuery.data?.defaultLocale), + [navigations, localeQuery.data?.defaultLocale] + ); + + const hasCache = !!configQuery.data?.isCacheEnabled; + + const isMutating = + createNavigationMutation.isPending || + updateNavigationMutation.isPending || + deleteNavigationsMutation.isPending || + purgeNavigationsMutation.isPending; + + const getLocaleVersions = (focused: Navigation) => + [focused].concat( + navigations.filter( + (navigation) => + navigation.documentId === focused.documentId && navigation.locale !== focused.locale + ) + ); + + const goToDetails = (navigation: Navigation) => { + navigate(`/plugins/${PLUGIN_ID}/${navigation.documentId}`); + }; + + const handleFormSubmit = (updated: Partial) => { + const options = { + onSuccess() { + hardReset(); + setOverlay(null); + toggleNotification({ + type: 'success', + message: formatMessage(getTrad('notification.navigation.submit')), + }); + }, + onError(error: unknown) { + console.warn(error); + toggleNotification({ + type: 'warning', + message: formatMessage(getTrad('notification.error.common')), + }); + }, + }; + + if (overlay?.type === 'edit') { + updateNavigationMutation.mutate(updated as Navigation, options); + } else { + createNavigationMutation.mutate(updated as Navigation, options); + } + }; + + const handleDelete = () => { + if (overlay?.type !== 'delete') { + return; + } + + deleteNavigationsMutation.mutate([overlay.navigation.documentId], { + onSuccess() { + hardReset(); + setOverlay(null); + }, + }); + }; + + const handlePurge = () => { + if (overlay?.type !== 'purge') { + return; + } + + purgeNavigationsMutation.mutate( + overlay.navigation ? [overlay.navigation.documentId] : undefined, + { + onSuccess() { + hardReset(); + setOverlay(null); + }, + } + ); + }; + + if ( + navigationsQuery.isPending || + localeQuery.isPending || + configQuery.isPending || + isLoadingForPermissions + ) { + return ; + } + + return ( + + + + + {hasCache && ( + + )} + + + ) + } + /> + + {defaultLocaleNavigations.length === 0 ? ( + } + content={formatMessage(getTrad('pages.overview.empty.description'))} + action={ + canUpdate && ( + + ) + } + /> + ) : ( + + + + + + + + + + + + {defaultLocaleNavigations.map((navigation) => ( + goToDetails(navigation)} + style={{ cursor: 'pointer' }} + > + + + + + + + ))} + +
+ + {formatMessage(getTrad('popup.navigation.manage.table.name'))} + + + + {formatMessage(getTrad('popup.navigation.manage.table.locale'))} + + + + {formatMessage(getTrad('popup.navigation.manage.table.visibility'))} + + + + {formatMessage(getTrad('pages.overview.table.items'))} + + + + {formatMessage(getTrad('pages.overview.table.actions'))} + +
+ + {navigation.name} + + + + {getLocaleVersions(navigation) + .map(({ locale }) => locale) + .join(', ')} + + + + + {navigation.visible + ? formatMessage(getTrad('popup.navigation.form.visible.toggle.visible')) + : formatMessage(getTrad('popup.navigation.form.visible.toggle.hidden'))} + + + + {navigation.items.length} + event.stopPropagation()}> + + {canUpdate && ( + setOverlay({ type: 'edit', navigation })} + label={formatMessage(getTrad('popup.navigation.manage.button.edit'))} + variant="ghost" + > + + + )} + {canUpdate && ( + setOverlay({ type: 'delete', navigation })} + label={formatMessage(getTrad('popup.navigation.manage.button.delete'))} + variant="ghost" + > + + + )} + {canUpdate && hasCache && ( + setOverlay({ type: 'purge', navigation })} + label={formatMessage(getTrad('popup.navigation.manage.button.purge'))} + variant="ghost" + > + + + )} + +
+ )} + + {(overlay?.type === 'create' || overlay?.type === 'edit') && ( + >( + (acc, { name }) => + overlay.type === 'edit' && name === overlay.navigation.name + ? acc + : acc.concat([name]), + [] + )} + isLoading={isMutating} + onClose={() => setOverlay(null)} + onSubmit={handleFormSubmit} + /> + )} + + {overlay?.type === 'delete' && ( + } + onConfirm={handleDelete} + onCancel={() => setOverlay(null)} + > + {`${formatMessage(getTrad('popup.navigation.manage.delete.header'))} "${overlay.navigation.name}"`} + + )} + + {overlay?.type === 'purge' && ( + } + onConfirm={handlePurge} + onCancel={() => setOverlay(null)} + > + {overlay.navigation + ? `${formatMessage(getTrad('popup.navigation.manage.purge.header'))} "${overlay.navigation.name}"` + : formatMessage(getTrad('popup.navigation.manage.purge.header'))} + + )} +
+
+
+ ); +}; + +export { OverviewPage }; diff --git a/admin/src/pages/OverviewPage/types.ts b/admin/src/pages/OverviewPage/types.ts new file mode 100644 index 00000000..d2d08c31 --- /dev/null +++ b/admin/src/pages/OverviewPage/types.ts @@ -0,0 +1,8 @@ +import { NavigationSchema } from '../../api/validators'; + +export type Navigation = Pick< + NavigationSchema, + 'id' | 'items' | 'name' | 'locale' | 'visible' | 'documentId' | 'slug' +>; + +export type NewNavigation = Omit; diff --git a/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx b/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx index 66ff16f7..472ee9b0 100644 --- a/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx +++ b/admin/src/pages/SettingsPage/components/AdditionalSettingsPanel/index.tsx @@ -83,6 +83,25 @@ export const AdditionalSettingsPanel = () => { /> + + + + handleChange(eventOrPath, !values.isOverviewUiEnabled, onChange) + } + onLabel={formatMessage(getTrad('components.toggle.enabled'))} + offLabel={formatMessage(getTrad('components.toggle.disabled'))} + disabled={restartStatus.required} + width="100%" + /> + + {configQuery.data?.isCachePluginEnabled && ( { return { diff --git a/server/src/config/index.ts b/server/src/config/index.ts index db8c18ae..1f257c6f 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -16,5 +16,6 @@ export default { cascadeMenuAttached: true, preferCustomContentTypes: false, isCacheEnabled: false, + isOverviewUiEnabled: false, }, }; diff --git a/server/src/config/setup.ts b/server/src/config/setup.ts index 08dd2d70..044d287b 100644 --- a/server/src/config/setup.ts +++ b/server/src/config/setup.ts @@ -50,6 +50,7 @@ export const configSetup = async ({ cascadeMenuAttached: getWithFallback('cascadeMenuAttached'), preferCustomContentTypes: getWithFallback('preferCustomContentTypes'), isCacheEnabled: getWithFallback('isCacheEnabled'), + isOverviewUiEnabled: getWithFallback('isOverviewUiEnabled'), }); handleDeletedContentTypes(config, { strapi }); diff --git a/server/src/dtos/config.ts b/server/src/dtos/config.ts index fa65790e..4bcfaa17 100644 --- a/server/src/dtos/config.ts +++ b/server/src/dtos/config.ts @@ -37,4 +37,5 @@ export type NavigationPluginConfigDTO = Pick< isGQLPluginEnabled: boolean | undefined; isCachePluginEnabled: boolean | undefined; isCacheEnabled: boolean | undefined; + isOverviewUiEnabled: boolean | undefined; }; diff --git a/server/src/schemas/config.ts b/server/src/schemas/config.ts index 6a0dbb4c..acd1feef 100644 --- a/server/src/schemas/config.ts +++ b/server/src/schemas/config.ts @@ -67,4 +67,5 @@ export const configSchema = z.object({ cascadeMenuAttached: z.boolean(), preferCustomContentTypes: z.boolean(), isCacheEnabled: z.boolean().optional(), + isOverviewUiEnabled: z.boolean().optional(), }); diff --git a/server/src/services/admin/admin.ts b/server/src/services/admin/admin.ts index bc7e7728..c3521d28 100644 --- a/server/src/services/admin/admin.ts +++ b/server/src/services/admin/admin.ts @@ -78,6 +78,7 @@ const adminService = (context: { strapi: Core.Strapi }) => ({ pathDefaultFields, allowedLevels, preferCustomContentTypes, + isOverviewUiEnabled, } = config; const isGQLPluginEnabled = !!context.strapi.plugin('graphql'); @@ -108,6 +109,7 @@ const adminService = (context: { strapi: Core.Strapi }) => ({ isGQLPluginEnabled: viaSettingsPage ? isGQLPluginEnabled : undefined, cascadeMenuAttached, preferCustomContentTypes, + isOverviewUiEnabled: !!isOverviewUiEnabled, }; if (additionalFields.includes('audience')) { diff --git a/server/tests/config/setup.test.ts b/server/tests/config/setup.test.ts index 9efecbfb..41415ec0 100644 --- a/server/tests/config/setup.test.ts +++ b/server/tests/config/setup.test.ts @@ -56,6 +56,7 @@ describe('Navigation', () => { navigationItemRelated: expect.any(Array), }), isCacheEnabled: expect.any(Boolean), + isOverviewUiEnabled: expect.any(Boolean), pathDefaultFields: expect.any(Object), preferCustomContentTypes: false, });