From ea0fc2822a1a4952cac1a124f22e0bd6bf1b3159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= <8087692+remidej@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:36:54 +0100 Subject: [PATCH] feat(cm): set up history page (#19309) * feat(cm): set up history page * feat: add injected component * fix: use React.useId * fix: typo Co-authored-by: markkaylor --------- Co-authored-by: markkaylor --- packages/core/admin/admin/src/StrapiApp.tsx | 8 ++++ .../history/components/InjectedLink.tsx | 25 ++++++++++ .../history/components/VersionDetails.tsx | 44 ++++++++++++++++++ .../history/components/VersionsList.tsx | 20 ++++++++ .../content-manager/history/pages/History.tsx | 46 +++++++++++++++++++ .../history/pages/tests/History.test.tsx | 41 +++++++++++++++++ .../src/content-manager/history/routes.tsx | 39 ++++++++++++++-- .../admin/src/content-manager/pages/App.tsx | 14 ++++-- .../admin/src/content-manager/routes.tsx | 3 +- .../core/admin/admin/src/pages/Layout.tsx | 14 ++++-- .../core/admin/admin/src/translations/en.json | 1 + 11 files changed, 243 insertions(+), 12 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/history/components/InjectedLink.tsx create mode 100644 packages/core/admin/admin/src/content-manager/history/components/VersionDetails.tsx create mode 100644 packages/core/admin/admin/src/content-manager/history/components/VersionsList.tsx create mode 100644 packages/core/admin/admin/src/content-manager/history/pages/History.tsx create mode 100644 packages/core/admin/admin/src/content-manager/history/pages/tests/History.test.tsx diff --git a/packages/core/admin/admin/src/StrapiApp.tsx b/packages/core/admin/admin/src/StrapiApp.tsx index 02a5ade6cb9..0720ff45743 100644 --- a/packages/core/admin/admin/src/StrapiApp.tsx +++ b/packages/core/admin/admin/src/StrapiApp.tsx @@ -31,6 +31,7 @@ import { } from './components/InjectionZone'; import { Providers } from './components/Providers'; import { HOOKS } from './constants'; +import { InjectedLink } from './content-manager/history/components/InjectedLink'; import { routes as cmRoutes } from './content-manager/routes'; import { Components, Component } from './core/apis/Components'; import { CustomFields } from './core/apis/CustomFields'; @@ -333,6 +334,13 @@ class StrapiApp { } }); + // TODO: remove once we can add the link via a document action instead + this.injectContentManagerComponent('editView', 'right-links', { + name: 'history', + Component: InjectedLink, + slug: 'history', + }); + if (isFunction(this.customBootstrapConfiguration)) { this.customBootstrapConfiguration({ addComponents: this.addComponents, diff --git a/packages/core/admin/admin/src/content-manager/history/components/InjectedLink.tsx b/packages/core/admin/admin/src/content-manager/history/components/InjectedLink.tsx new file mode 100644 index 00000000000..0103c959224 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/history/components/InjectedLink.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { LinkButton } from '@strapi/design-system/v2'; +import { useQueryParams } from '@strapi/helper-plugin'; +import { stringify } from 'qs'; +import { NavLink } from 'react-router-dom'; + +/** + * This is a temporary component to easily access the history page. + * TODO: delete it when the document actions API is ready + */ + +const InjectedLink = () => { + const [{ query }] = useQueryParams<{ plugins?: Record }>(); + const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false }); + + return ( + // @ts-expect-error - types are not inferred correctly through the as prop. + + History + + ); +}; + +export { InjectedLink }; diff --git a/packages/core/admin/admin/src/content-manager/history/components/VersionDetails.tsx b/packages/core/admin/admin/src/content-manager/history/components/VersionDetails.tsx new file mode 100644 index 00000000000..7dca3f3091e --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/history/components/VersionDetails.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; + +import { ContentLayout, HeaderLayout, Main, Typography } from '@strapi/design-system'; +import { Link } from '@strapi/design-system/v2'; +import { ArrowLeft } from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import { NavLink, useNavigate } from 'react-router-dom'; + +const VersionDetails = () => { + const { formatMessage } = useIntl(); + const navigate = useNavigate(); + const headerId = React.useId(); + + return ( +
+ } + onClick={(e) => { + e.preventDefault(); + navigate(-1); + }} + as={NavLink} + // @ts-expect-error - types are not inferred correctly through the as prop. + to="" + > + {formatMessage({ + id: 'global.back', + defaultMessage: 'Back', + })} + + } + /> + + Content + +
+ ); +}; + +export { VersionDetails }; diff --git a/packages/core/admin/admin/src/content-manager/history/components/VersionsList.tsx b/packages/core/admin/admin/src/content-manager/history/components/VersionsList.tsx new file mode 100644 index 00000000000..2e14e7898e0 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/history/components/VersionsList.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import { Box, Typography } from '@strapi/design-system'; + +const VersionsList = () => { + return ( + + Sidebar + + ); +}; + +export { VersionsList }; diff --git a/packages/core/admin/admin/src/content-manager/history/pages/History.tsx b/packages/core/admin/admin/src/content-manager/history/pages/History.tsx new file mode 100644 index 00000000000..7b75896bede --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/history/pages/History.tsx @@ -0,0 +1,46 @@ +import { Flex } from '@strapi/design-system'; +import { LoadingIndicatorPage } from '@strapi/helper-plugin'; +import { Helmet } from 'react-helmet'; +import { useIntl } from 'react-intl'; +import { useParams } from 'react-router-dom'; + +import { useContentTypeLayout } from '../../hooks/useLayouts'; +import { VersionDetails } from '../components/VersionDetails'; +import { VersionsList } from '../components/VersionsList'; + +const HistoryPage = () => { + const { formatMessage } = useIntl(); + const { slug } = useParams<{ + collectionType: string; + singleType: string; + slug: string; + }>(); + + const { isLoading, layout } = useContentTypeLayout(slug); + + if (isLoading) { + return ; + } + + return ( + <> + + + + + + + ); +}; + +export { HistoryPage }; diff --git a/packages/core/admin/admin/src/content-manager/history/pages/tests/History.test.tsx b/packages/core/admin/admin/src/content-manager/history/pages/tests/History.test.tsx new file mode 100644 index 00000000000..df1fd2ab266 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/history/pages/tests/History.test.tsx @@ -0,0 +1,41 @@ +import { render, screen, waitFor } from '@tests/utils'; +import { Route, Routes } from 'react-router-dom'; + +import { HistoryPage } from '../History'; + +describe('History page', () => { + it('renders single type correctly', async () => { + render( + + } /> + , + { + initialEntries: ['/content-manager/single-types/api::address.address/history'], + } + ); + + await waitFor(() => { + expect(screen.queryByTestId('loader')).not.toBeInTheDocument(); + }); + expect(document.title).toBe('Address history'); + }); + + it('renders collection type correctly', async () => { + render( + + } + /> + , + { + initialEntries: ['/content-manager/collection-types/api::address.address/1/history'], + } + ); + + await waitFor(() => { + expect(screen.queryByTestId('loader')).not.toBeInTheDocument(); + }); + expect(document.title).toBe('Address history'); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/history/routes.tsx b/packages/core/admin/admin/src/content-manager/history/routes.tsx index 8445a47e88f..82eedefae19 100644 --- a/packages/core/admin/admin/src/content-manager/history/routes.tsx +++ b/packages/core/admin/admin/src/content-manager/history/routes.tsx @@ -1,9 +1,42 @@ /* eslint-disable check-file/filename-naming-convention */ -import { type RouteObject } from 'react-router-dom'; +import { useLocation, type RouteObject, matchRoutes } from 'react-router-dom'; /** * These routes will be merged with the rest of the Content Manager routes */ -const routes: RouteObject[] = []; +const routes: RouteObject[] = [ + { + path: ':collectionType/:slug/:id/history', + lazy: async () => { + const { HistoryPage } = await import('./pages/History'); -export { routes }; + return { + Component: HistoryPage, + }; + }, + }, + { + path: ':singleType/:slug/history', + lazy: async () => { + const { HistoryPage } = await import('./pages/History'); + + return { + Component: HistoryPage, + }; + }, + }, +]; + +/** + * Used to determine if we're on a history route from the admin and the content manager, + * so that we can hide the left menus on all history routes + */ +function useIsHistoryRoute() { + const location = useLocation(); + const historyRoutes = routes.map((route) => ({ path: `content-manager/${route.path}` })); + const matches = matchRoutes(historyRoutes, location); + + return Boolean(matches); +} + +export { routes, useIsHistoryRoute }; diff --git a/packages/core/admin/admin/src/content-manager/pages/App.tsx b/packages/core/admin/admin/src/content-manager/pages/App.tsx index 4f094f7931e..3e80a843060 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App.tsx +++ b/packages/core/admin/admin/src/content-manager/pages/App.tsx @@ -14,6 +14,7 @@ import { CardDragPreview } from '../components/DragPreviews/CardDragPreview'; import { ComponentDragPreview } from '../components/DragPreviews/ComponentDragPreview'; import { RelationDragPreview } from '../components/DragPreviews/RelationDragPreview'; import { LeftMenu } from '../components/LeftMenu'; +import { useIsHistoryRoute } from '../history/routes'; import { useContentManagerInitData } from '../hooks/useContentManagerInitData'; import { ItemTypes } from '../utils/dragAndDrop'; import { getTranslation } from '../utils/translations'; @@ -38,6 +39,9 @@ const App = () => { const { startSection } = useGuidedTour(); const startSectionRef = React.useRef(startSection); + // Check if we're on a history route to known if we should render the left menu + const isHistoryRoute = useIsHistoryRoute(); + React.useEffect(() => { if (startSectionRef.current) { startSectionRef.current('contentManager'); @@ -100,10 +104,14 @@ const App = () => { defaultMessage: 'Content Manager', })} /> - }> - + {isHistoryRoute ? ( - + ) : ( + }> + + + + )} ); }; diff --git a/packages/core/admin/admin/src/content-manager/routes.tsx b/packages/core/admin/admin/src/content-manager/routes.tsx index 57a81d0df4c..a29ec457dbe 100644 --- a/packages/core/admin/admin/src/content-manager/routes.tsx +++ b/packages/core/admin/admin/src/content-manager/routes.tsx @@ -1,6 +1,5 @@ /* eslint-disable check-file/filename-naming-convention */ -import { UID } from '@strapi/types'; -import { Navigate, RouteObject, useLoaderData } from 'react-router-dom'; +import { Navigate, type RouteObject, useLoaderData } from 'react-router-dom'; import { routes as historyRoutes } from './history/routes'; diff --git a/packages/core/admin/admin/src/pages/Layout.tsx b/packages/core/admin/admin/src/pages/Layout.tsx index cdffb2fca38..2b119982861 100644 --- a/packages/core/admin/admin/src/pages/Layout.tsx +++ b/packages/core/admin/admin/src/pages/Layout.tsx @@ -22,6 +22,7 @@ import { Onboarding } from '../components/Onboarding'; import { PluginsInitializer } from '../components/PluginsInitializer'; import { PrivateRoute } from '../components/PrivateRoute'; import { RBACProvider } from '../components/RBACProvider'; +import { useIsHistoryRoute } from '../content-manager/history/routes'; import { useAuth } from '../features/Auth'; import { useConfiguration } from '../features/Configuration'; import { useMenu } from '../hooks/useMenu'; @@ -129,6 +130,9 @@ const AdminLayout = () => { trackUsage('didAccessAuthenticatedAdministration'); }); + // Check if we're on a history route to know if we should render the left menu + const isHistoryRoute = useIsHistoryRoute(); + // We don't need to wait for the release query to be fetched before rendering the plugins // however, we need the appInfos and the permissions if (isLoadingMenu || isLoadingAppInfo || isLoadingPermissions) { @@ -157,10 +161,12 @@ const AdminLayout = () => { {formatMessage({ id: 'skipToContent', defaultMessage: 'Skip to content' })} - + {!isHistoryRoute && ( + + )} diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index b1d434583b2..1881c42c139 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -904,6 +904,7 @@ "content-manager.utils.data-loaded": "The {number, plural, =1 {entry has} other {entries have}} successfully been loaded", "content-manager.listView.validation.errors.title": "Action required", "content-manager.listView.validation.errors.message": "Please make sure all fields are valid before publishing (required field, min/max character limit, etc.)", + "content-manager.history.page-title": "{contentType} history", "dark": "Dark", "form.button.continue": "Continue", "form.button.done": "Done",