mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 15:44:59 +00:00
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 <mark.kaylor@strapi.io> --------- Co-authored-by: markkaylor <mark.kaylor@strapi.io>
This commit is contained in:
parent
df4560117a
commit
ea0fc2822a
@ -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,
|
||||
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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,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',
|
||||
})}
|
||||
/>
|
||||
<Layout sideNav={<LeftMenu />}>
|
||||
<DragLayer renderItem={renderDraglayerItem} />
|
||||
{isHistoryRoute ? (
|
||||
<Outlet />
|
||||
</Layout>
|
||||
) : (
|
||||
<Layout sideNav={<LeftMenu />}>
|
||||
<DragLayer renderItem={renderDraglayerItem} />
|
||||
<Outlet />
|
||||
</Layout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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' })}
|
||||
</SkipToContent>
|
||||
<Flex alignItems="flex-start">
|
||||
<LeftMenu
|
||||
generalSectionLinks={generalSectionLinks}
|
||||
pluginsSectionLinks={pluginsSectionLinks}
|
||||
/>
|
||||
{!isHistoryRoute && (
|
||||
<LeftMenu
|
||||
generalSectionLinks={generalSectionLinks}
|
||||
pluginsSectionLinks={pluginsSectionLinks}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1}>
|
||||
<Outlet />
|
||||
<GuidedTourModal />
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user