chore(content-releases): show Banner reached limit max releases (#19276)

* chore(content-releases): add alert reached max limit pending releases

* chore(content-releases): use max pending releases number from config

* chore(content-releases): change limits string content

* chore(content-releases): unit test to check the limit banner

* chore(content-releases): fix ts error

* chore(content-releases): fix review comments

* chore(content-releases): refactor the solution to use useLicenseLimits

* chore(content-releases): fix review comments

* chore(content-releases): fix type error

* chore(content-releases): fix HeaderLayout wrong height because of subtitle empty on loading

* chore(content-releases): remove ReleaseLayout component

* chore(content-releases): remove useless translation

---------

Co-authored-by: Fernando Chavez <fernando.chavez@strapi.io>
This commit is contained in:
Simone 2024-01-30 12:03:33 +01:00 committed by GitHub
parent 2d371a2d60
commit cae3a5a17d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 60 deletions

View File

@ -5,3 +5,5 @@ export type { Store } from './core/store/configure';
export type { SanitizedAdminUser } from '../../shared/contracts/shared'; export type { SanitizedAdminUser } from '../../shared/contracts/shared';
export { useDocument as unstable_useDocument } from './hooks/useDocument'; export { useDocument as unstable_useDocument } from './hooks/useDocument';
// TODO: Replace this export with the same hook exported from the @strapi/admin/strapi-admin/ee in another iteration of this solution
export { useLicenseLimits } from '../../ee/admin/src/hooks/useLicenseLimits';

View File

@ -172,6 +172,13 @@ export interface ReviewWorkflowsFeature {
options?: { numberOfWorkflows: number | null; stagesPerWorkflow: number | null }; options?: { numberOfWorkflows: number | null; stagesPerWorkflow: number | null };
} }
export interface ContentReleasesFeature {
name: 'cms-content-releases';
options?: {
maximumReleases: number;
};
}
/** /**
* TODO: this response needs refactoring because we're mixing the admin seat limit info with * TODO: this response needs refactoring because we're mixing the admin seat limit info with
* regular EE feature info. * regular EE feature info.
@ -185,7 +192,7 @@ export declare namespace GetLicenseLimitInformation {
data: { data: {
currentActiveUserCount: number; currentActiveUserCount: number;
enforcementUserCount: number; enforcementUserCount: number;
features: (SSOFeature | AuditLogsFeature | ReviewWorkflowsFeature)[]; features: (SSOFeature | AuditLogsFeature | ReviewWorkflowsFeature | ContentReleasesFeature)[];
isHostedOnStrapiCloud: boolean; isHostedOnStrapiCloud: boolean;
licenseLimitStatus: unknown; licenseLimitStatus: unknown;
permittedSeats: number; permittedSeats: number;

View File

@ -1,6 +1,9 @@
import * as React from 'react'; import * as React from 'react';
// TODO: Replace this import with the same hook exported from the @strapi/admin/strapi-admin/ee in another iteration of this solution
import { useLicenseLimits } from '@strapi/admin/strapi-admin';
import { import {
Alert,
Box, Box,
Button, Button,
ContentLayout, ContentLayout,
@ -44,57 +47,6 @@ import {
useCreateReleaseMutation, useCreateReleaseMutation,
} from '../services/release'; } from '../services/release';
/* -------------------------------------------------------------------------------------------------
* ReleasesLayout
* -----------------------------------------------------------------------------------------------*/
interface ReleasesLayoutProps {
isLoading?: boolean;
totalReleases?: number;
onClickAddRelease: () => void;
children: React.ReactNode;
}
export const ReleasesLayout = ({
isLoading,
totalReleases,
onClickAddRelease,
children,
}: ReleasesLayoutProps) => {
const { formatMessage } = useIntl();
return (
<Main aria-busy={isLoading}>
<HeaderLayout
title={formatMessage({
id: 'content-releases.pages.Releases.title',
defaultMessage: 'Releases',
})}
subtitle={
!isLoading &&
formatMessage(
{
id: 'content-releases.pages.Releases.header-subtitle',
defaultMessage:
'{number, plural, =0 {No releases} one {# release} other {# releases}}',
},
{ number: totalReleases }
)
}
primaryAction={
<CheckPermissions permissions={PERMISSIONS.create}>
<Button startIcon={<Plus />} onClick={onClickAddRelease}>
{formatMessage({
id: 'content-releases.header.actions.add-release',
defaultMessage: 'New release',
})}
</Button>
</CheckPermissions>
}
/>
{children}
</Main>
);
};
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
* ReleasesGrid * ReleasesGrid
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
@ -177,6 +129,15 @@ interface CustomLocationState {
errors?: Record<'code', string>[]; errors?: Record<'code', string>[];
} }
const StyledAlert = styled(Alert)`
button {
display: none;
}
p + div {
margin-left: auto;
}
`;
const INITIAL_FORM_VALUES = { const INITIAL_FORM_VALUES = {
name: '', name: '',
} satisfies FormValues; } satisfies FormValues;
@ -192,7 +153,10 @@ const ReleasesPage = () => {
const [{ query }, setQuery] = useQueryParams<GetReleasesQueryParams>(); const [{ query }, setQuery] = useQueryParams<GetReleasesQueryParams>();
const response = useGetReleasesQuery(query); const response = useGetReleasesQuery(query);
const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation(); const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
const { getFeature } = useLicenseLimits();
const { maximumNumberOfPendingReleases = 3 } = getFeature('cms-content-releases') as {
maximumNumberOfPendingReleases: number;
};
const { isLoading, isSuccess, isError } = response; const { isLoading, isSuccess, isError } = response;
const activeTab = response?.currentData?.meta?.activeTab || 'pending'; const activeTab = response?.currentData?.meta?.activeTab || 'pending';
const activeTabIndex = ['pending', 'done'].indexOf(activeTab); const activeTabIndex = ['pending', 'done'].indexOf(activeTab);
@ -229,15 +193,14 @@ const ReleasesPage = () => {
if (isLoading) { if (isLoading) {
return ( return (
<ReleasesLayout onClickAddRelease={toggleAddReleaseModal} isLoading> <Main aria-busy={isLoading}>
<ContentLayout> <LoadingIndicatorPage />
<LoadingIndicatorPage /> </Main>
</ContentLayout>
</ReleasesLayout>
); );
} }
const totalReleases = (isSuccess && response.currentData?.meta?.pagination?.total) || 0; const totalReleases = (isSuccess && response.currentData?.meta?.pagination?.total) || 0;
const hasReachedMaximumPendingReleases = totalReleases >= maximumNumberOfPendingReleases;
const handleTabChange = (index: number) => { const handleTabChange = (index: number) => {
setQuery({ setQuery({
@ -283,9 +246,64 @@ const ReleasesPage = () => {
}; };
return ( return (
<ReleasesLayout onClickAddRelease={toggleAddReleaseModal} totalReleases={totalReleases}> <Main aria-busy={isLoading}>
<HeaderLayout
title={formatMessage({
id: 'content-releases.pages.Releases.title',
defaultMessage: 'Releases',
})}
subtitle={formatMessage(
{
id: 'content-releases.pages.Releases.header-subtitle',
defaultMessage: '{number, plural, =0 {No releases} one {# release} other {# releases}}',
},
{ number: totalReleases }
)}
primaryAction={
<CheckPermissions permissions={PERMISSIONS.create}>
<Button
startIcon={<Plus />}
onClick={toggleAddReleaseModal}
disabled={hasReachedMaximumPendingReleases}
>
{formatMessage({
id: 'content-releases.header.actions.add-release',
defaultMessage: 'New release',
})}
</Button>
</CheckPermissions>
}
/>
<ContentLayout> <ContentLayout>
<> <>
{activeTab === 'pending' && hasReachedMaximumPendingReleases && (
<StyledAlert
marginBottom={6}
action={
<Link href="https://strapi.io/pricing-cloud" isExternal>
{formatMessage({
id: 'content-releases.pages.Releases.max-limit-reached.action',
defaultMessage: 'Explore plans',
})}
</Link>
}
title={formatMessage(
{
id: 'content-releases.pages.Releases.max-limit-reached.title',
defaultMessage:
'You have reached the {number} pending {number, plural, one {release} other {releases}} limit.',
},
{ number: maximumNumberOfPendingReleases }
)}
onClose={() => {}}
closeLabel=""
>
{formatMessage({
id: 'content-releases.pages.Releases.max-limit-reached.message',
defaultMessage: 'Upgrade to manage an unlimited number of releases.',
})}
</StyledAlert>
)}
<TabGroup <TabGroup
label={formatMessage({ label={formatMessage({
id: 'content-releases.pages.Releases.tab-group.label', id: 'content-releases.pages.Releases.tab-group.label',
@ -355,7 +373,7 @@ const ReleasesPage = () => {
initialValues={INITIAL_FORM_VALUES} initialValues={INITIAL_FORM_VALUES}
/> />
)} )}
</ReleasesLayout> </Main>
); );
}; };

View File

@ -12,6 +12,21 @@ jest.mock('@strapi/helper-plugin', () => ({
CheckPermissions: ({ children }: { children: JSX.Element }) => <div>{children}</div>, CheckPermissions: ({ children }: { children: JSX.Element }) => <div>{children}</div>,
})); }));
jest.mock('@strapi/admin/strapi-admin', () => ({
...jest.requireActual('@strapi/admin/strapi-admin'),
useLicenseLimits: jest.fn().mockReturnValue({
isLoading: false,
isError: false,
license: {
enforcementUserCount: 10,
licenseLimitStatus: '',
permittedSeats: 3,
isHostedOnStrapiCloud: false,
},
getFeature: jest.fn().mockReturnValue({ maximumReleases: 3 }),
}),
}));
describe('Releases home page', () => { describe('Releases home page', () => {
it('renders the tab content correctly when there are no releases', async () => { it('renders the tab content correctly when there are no releases', async () => {
server.use( server.use(
@ -54,5 +69,11 @@ describe('Releases home page', () => {
const lastEntry = screen.getByRole('heading', { level: 3, name: 'entry 17' }); const lastEntry = screen.getByRole('heading', { level: 3, name: 'entry 17' });
expect(lastEntry).toBeInTheDocument(); expect(lastEntry).toBeInTheDocument();
// Check if you reached the maximum number of releases for license
const newReleaseButton = screen.queryByRole('button', { name: /new release/i });
expect(newReleaseButton).toBeDisabled();
const limitReachedMessage = screen.getByText(/you have reached the 3 pending releases limit/i);
expect(limitReachedMessage).toBeInTheDocument();
}); });
}); });

View File

@ -17,6 +17,9 @@
"plugin.name": "Releases", "plugin.name": "Releases",
"pages.Releases.title": "Releases", "pages.Releases.title": "Releases",
"pages.Releases.header-subtitle": "{number, plural, =0 {No releases} one {# release} other {# releases}}", "pages.Releases.header-subtitle": "{number, plural, =0 {No releases} one {# release} other {# releases}}",
"pages.Releases.max-limit-reached.title": "You have reached the {number} pending {number, plural, one {release} other {releases}} limit.",
"pages.Releases.max-limit-reached.message": "Upgrade to manage an unlimited number of releases.",
"pages.Releases.max-limit-reached.action": "Explore plans",
"header.actions.add-release": "New Release", "header.actions.add-release": "New Release",
"header.actions.refresh": "Refresh", "header.actions.refresh": "Refresh",
"header.actions.publish": "Publish", "header.actions.publish": "Publish",