mirror of
https://github.com/strapi/strapi.git
synced 2025-08-30 19:56:05 +00:00
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:
parent
2d371a2d60
commit
cae3a5a17d
@ -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';
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user