Skip to content

Commit

Permalink
feat(cm): set up history page (#19309)
Browse files Browse the repository at this point in the history
* feat(cm): set up history page

* feat: add injected component

* fix: use React.useId

* fix: typo

Co-authored-by: markkaylor <mark.kaylor@strapi.io>

---------

Co-authored-by: markkaylor <mark.kaylor@strapi.io>
  • Loading branch information
remidej and markkaylor committed Jan 25, 2024
1 parent df45601 commit ea0fc28
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 12 deletions.
8 changes: 8 additions & 0 deletions packages/core/admin/admin/src/StrapiApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }>();
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });

return (
// @ts-expect-error - types are not inferred correctly through the as prop.
<LinkButton as={NavLink} variant="primary" to={`history?${pluginsQueryParams}`}>
History
</LinkButton>
);
};

export { InjectedLink };
Original file line number Diff line number Diff line change
@@ -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 (
<Main grow={1} labelledBy={headerId}>
<HeaderLayout
id={headerId}
title="History"
navigationAction={
<Link
startIcon={<ArrowLeft />}
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',
})}
</Link>
}
/>
<ContentLayout>
<Typography>Content</Typography>
</ContentLayout>
</Main>
);
};

export { VersionDetails };
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';

import { Box, Typography } from '@strapi/design-system';

const VersionsList = () => {
return (
<Box
width="320px"
minHeight="100vh"
background="neutral0"
borderColor="neutral200"
borderWidth="0 0 0 1px"
borderStyle="solid"
>
<Typography>Sidebar</Typography>
</Box>
);
};

export { VersionsList };
Original file line number Diff line number Diff line change
@@ -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 <LoadingIndicatorPage />;
}

return (
<>
<Helmet
title={formatMessage(
{
id: 'content-manager.history.page-title',
defaultMessage: '{contentType} history',
},
{
contentType: layout?.contentType.info.displayName,
}
)}
/>
<Flex direction="row" alignItems="flex-start">
<VersionDetails />
<VersionsList />
</Flex>
</>
);
};

export { HistoryPage };
Original file line number Diff line number Diff line change
@@ -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(
<Routes>
<Route path="/content-manager/:singleType/:slug/history" element={<HistoryPage />} />
</Routes>,
{
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(
<Routes>
<Route
path="/content-manager/:collectionType/:slug/:id/history"
element={<HistoryPage />}
/>
</Routes>,
{
initialEntries: ['/content-manager/collection-types/api::address.address/1/history'],
}
);

await waitFor(() => {
expect(screen.queryByTestId('loader')).not.toBeInTheDocument();
});
expect(document.title).toBe('Address history');
});
});
39 changes: 36 additions & 3 deletions packages/core/admin/admin/src/content-manager/history/routes.tsx
Original file line number Diff line number Diff line change
@@ -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 };
14 changes: 11 additions & 3 deletions packages/core/admin/admin/src/content-manager/pages/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -100,10 +104,14 @@ const App = () => {
defaultMessage: 'Content Manager',
})}
/>
<Layout sideNav={<LeftMenu />}>
<DragLayer renderItem={renderDraglayerItem} />
{isHistoryRoute ? (
<Outlet />
</Layout>
) : (
<Layout sideNav={<LeftMenu />}>
<DragLayer renderItem={renderDraglayerItem} />
<Outlet />
</Layout>
)}
</>
);
};
Expand Down
3 changes: 1 addition & 2 deletions packages/core/admin/admin/src/content-manager/routes.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
14 changes: 10 additions & 4 deletions packages/core/admin/admin/src/pages/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -157,10 +161,12 @@ const AdminLayout = () => {
{formatMessage({ id: 'skipToContent', defaultMessage: 'Skip to content' })}
</SkipToContent>
<Flex alignItems="flex-start">
<LeftMenu
generalSectionLinks={generalSectionLinks}
pluginsSectionLinks={pluginsSectionLinks}
/>
{!isHistoryRoute && (
<LeftMenu
generalSectionLinks={generalSectionLinks}
pluginsSectionLinks={pluginsSectionLinks}
/>
)}
<Box flex={1}>
<Outlet />
<GuidedTourModal />
Expand Down
1 change: 1 addition & 0 deletions packages/core/admin/admin/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit ea0fc28

Please sign in to comment.