diff --git a/packages/core/admin/admin/src/index.ts b/packages/core/admin/admin/src/index.ts
index a662976485..c7e1e454cc 100644
--- a/packages/core/admin/admin/src/index.ts
+++ b/packages/core/admin/admin/src/index.ts
@@ -5,3 +5,5 @@ export type { Store } from './core/store/configure';
export type { SanitizedAdminUser } from '../../shared/contracts/shared';
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';
diff --git a/packages/core/admin/shared/contracts/admin.ts b/packages/core/admin/shared/contracts/admin.ts
index 190221a447..2e1ee1872c 100644
--- a/packages/core/admin/shared/contracts/admin.ts
+++ b/packages/core/admin/shared/contracts/admin.ts
@@ -172,6 +172,13 @@ export interface ReviewWorkflowsFeature {
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
* regular EE feature info.
@@ -185,7 +192,7 @@ export declare namespace GetLicenseLimitInformation {
data: {
currentActiveUserCount: number;
enforcementUserCount: number;
- features: (SSOFeature | AuditLogsFeature | ReviewWorkflowsFeature)[];
+ features: (SSOFeature | AuditLogsFeature | ReviewWorkflowsFeature | ContentReleasesFeature)[];
isHostedOnStrapiCloud: boolean;
licenseLimitStatus: unknown;
permittedSeats: number;
diff --git a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx
index be2437fdfd..f07d0a2cca 100644
--- a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx
+++ b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx
@@ -1,6 +1,9 @@
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 {
+ Alert,
Box,
Button,
ContentLayout,
@@ -44,57 +47,6 @@ import {
useCreateReleaseMutation,
} 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 (
-
-
- } onClick={onClickAddRelease}>
- {formatMessage({
- id: 'content-releases.header.actions.add-release',
- defaultMessage: 'New release',
- })}
-
-
- }
- />
- {children}
-
- );
-};
-
/* -------------------------------------------------------------------------------------------------
* ReleasesGrid
* -----------------------------------------------------------------------------------------------*/
@@ -177,6 +129,15 @@ interface CustomLocationState {
errors?: Record<'code', string>[];
}
+const StyledAlert = styled(Alert)`
+ button {
+ display: none;
+ }
+ p + div {
+ margin-left: auto;
+ }
+`;
+
const INITIAL_FORM_VALUES = {
name: '',
} satisfies FormValues;
@@ -192,7 +153,10 @@ const ReleasesPage = () => {
const [{ query }, setQuery] = useQueryParams();
const response = useGetReleasesQuery(query);
const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
-
+ const { getFeature } = useLicenseLimits();
+ const { maximumNumberOfPendingReleases = 3 } = getFeature('cms-content-releases') as {
+ maximumNumberOfPendingReleases: number;
+ };
const { isLoading, isSuccess, isError } = response;
const activeTab = response?.currentData?.meta?.activeTab || 'pending';
const activeTabIndex = ['pending', 'done'].indexOf(activeTab);
@@ -229,15 +193,14 @@ const ReleasesPage = () => {
if (isLoading) {
return (
-
-
-
-
-
+
+
+
);
}
const totalReleases = (isSuccess && response.currentData?.meta?.pagination?.total) || 0;
+ const hasReachedMaximumPendingReleases = totalReleases >= maximumNumberOfPendingReleases;
const handleTabChange = (index: number) => {
setQuery({
@@ -283,9 +246,64 @@ const ReleasesPage = () => {
};
return (
-
+
+
+ }
+ onClick={toggleAddReleaseModal}
+ disabled={hasReachedMaximumPendingReleases}
+ >
+ {formatMessage({
+ id: 'content-releases.header.actions.add-release',
+ defaultMessage: 'New release',
+ })}
+
+
+ }
+ />
<>
+ {activeTab === 'pending' && hasReachedMaximumPendingReleases && (
+
+ {formatMessage({
+ id: 'content-releases.pages.Releases.max-limit-reached.action',
+ defaultMessage: 'Explore plans',
+ })}
+
+ }
+ 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.',
+ })}
+
+ )}
{
initialValues={INITIAL_FORM_VALUES}
/>
)}
-
+
);
};
diff --git a/packages/core/content-releases/admin/src/pages/tests/ReleasesPage.test.tsx b/packages/core/content-releases/admin/src/pages/tests/ReleasesPage.test.tsx
index 787fbc17b7..c9bfa0658a 100644
--- a/packages/core/content-releases/admin/src/pages/tests/ReleasesPage.test.tsx
+++ b/packages/core/content-releases/admin/src/pages/tests/ReleasesPage.test.tsx
@@ -12,6 +12,21 @@ jest.mock('@strapi/helper-plugin', () => ({
CheckPermissions: ({ children }: { children: JSX.Element }) => {children}
,
}));
+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', () => {
it('renders the tab content correctly when there are no releases', async () => {
server.use(
@@ -54,5 +69,11 @@ describe('Releases home page', () => {
const lastEntry = screen.getByRole('heading', { level: 3, name: 'entry 17' });
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();
});
});
diff --git a/packages/core/content-releases/admin/src/translations/en.json b/packages/core/content-releases/admin/src/translations/en.json
index b309e13271..cc80db47a4 100644
--- a/packages/core/content-releases/admin/src/translations/en.json
+++ b/packages/core/content-releases/admin/src/translations/en.json
@@ -17,6 +17,9 @@
"plugin.name": "Releases",
"pages.Releases.title": "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.refresh": "Refresh",
"header.actions.publish": "Publish",