diff --git a/e2e/tests/content-releases/releases-page.spec.ts b/e2e/tests/content-releases/releases-page.spec.ts index 2f57cc6c1c..a70bdbd375 100644 --- a/e2e/tests/content-releases/releases-page.spec.ts +++ b/e2e/tests/content-releases/releases-page.spec.ts @@ -70,7 +70,15 @@ describeOnCondition(edition === 'EE')('Releases page', () => { name: 'Date', }) .click(); - await page.getByRole('gridcell', { name: 'Sunday, March 3, 2024' }).click(); + + const formattedDate = new Date().toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); + + await page.getByRole('gridcell', { name: formattedDate }).click(); await page .getByRole('combobox', { diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 4a7ca2b70f..a7805ec82e 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -60,6 +60,8 @@ import { import { useTypedDispatch } from '../store/hooks'; import { getTimezoneOffset } from '../utils/time'; +import { getBadgeProps } from './ReleasesPage'; + import type { ReleaseAction, ReleaseActionGroupBy, @@ -350,7 +352,13 @@ export const ReleaseDetailsLayout = ({ + + {numberOfEntriesText + + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : '')} + + {release.status} + } navigationAction={ } to="/plugins/content-releases"> diff --git a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx index bb0a625a05..97bf508db6 100644 --- a/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleasesPage.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useLicenseLimits } from '@strapi/admin/strapi-admin'; import { Alert, + Badge, Box, Button, ContentLayout, @@ -39,7 +40,7 @@ import { useIntl } from 'react-intl'; import { useHistory, useLocation } from 'react-router-dom'; import styled from 'styled-components'; -import { GetReleases } from '../../../shared/contracts/releases'; +import { GetReleases, type Release } from '../../../shared/contracts/releases'; import { ReleaseModal, FormValues } from '../components/ReleaseModal'; import { PERMISSIONS } from '../constants'; import { isAxiosError } from '../services/axios'; @@ -62,6 +63,37 @@ const LinkCard = styled(Link)` display: block; `; +const CapitalizeRelativeTime = styled(RelativeTime)` + text-transform: capitalize; +`; + +const getBadgeProps = (status: Release['status']) => { + let color; + switch (status) { + case 'ready': + color = 'success'; + break; + case 'blocked': + color = 'warning'; + break; + case 'failed': + color = 'danger'; + break; + case 'done': + color = 'primary'; + break; + case 'empty': + default: + color = 'neutral'; + } + + return { + textColor: `${color}600`, + backgroundColor: `${color}100`, + borderColor: `${color}200`, + }; +}; + const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: ReleasesGridProps) => { const { formatMessage } = useIntl(); const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling'); @@ -89,7 +121,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases return ( - {releases.map(({ id, name, actions, scheduledAt }) => ( + {releases.map(({ id, name, actions, scheduledAt, status }) => ( - - {name} - - - {IsSchedulingEnabled ? ( - scheduledAt ? ( - + + + {name} + + + {IsSchedulingEnabled ? ( + scheduledAt ? ( + + ) : ( + formatMessage({ + id: 'content-releases.pages.Releases.not-scheduled', + defaultMessage: 'Not scheduled', + }) + ) ) : ( - formatMessage({ - id: 'content-releases.pages.Releases.not-scheduled', - defaultMessage: 'Not scheduled', - }) - ) - ) : ( - formatMessage( - { - id: 'content-releases.page.Releases.release-item.entries', - defaultMessage: - '{number, plural, =0 {No entries} one {# entry} other {# entries}}', - }, - { number: actions.meta.count } - ) - )} - + formatMessage( + { + id: 'content-releases.page.Releases.release-item.entries', + defaultMessage: + '{number, plural, =0 {No entries} one {# entry} other {# entries}}', + }, + { number: actions.meta.count } + ) + )} + + + {status} @@ -405,4 +440,4 @@ const ReleasesPage = () => { ); }; -export { ReleasesPage }; +export { ReleasesPage, getBadgeProps }; diff --git a/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx b/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx index e97d905dbb..0edc7893b3 100644 --- a/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx +++ b/packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx @@ -52,6 +52,9 @@ describe('Releases details page', () => { const releaseSubtitle = await screen.findAllByText('No entries'); expect(releaseSubtitle[0]).toBeInTheDocument(); + const releaseStatus = screen.getByText('empty'); + expect(releaseStatus).toBeInTheDocument(); + const moreButton = screen.getByRole('button', { name: 'Release edit and delete menu' }); expect(moreButton).toBeInTheDocument(); @@ -146,7 +149,7 @@ describe('Releases details page', () => { expect(tables).toHaveLength(2); }); - it('shows the right status', async () => { + it('shows the right status for unpublished release', async () => { server.use( rest.get('/content-releases/:releaseId', (req, res, ctx) => res(ctx.json(mockReleaseDetailsPageData.withActionsHeaderData)) @@ -160,7 +163,7 @@ describe('Releases details page', () => { ); render(, { - initialEntries: [{ pathname: `/content-releases/1` }], + initialEntries: [{ pathname: `/content-releases/2` }], }); const releaseTitle = await screen.findByText( @@ -168,6 +171,10 @@ describe('Releases details page', () => { ); expect(releaseTitle).toBeInTheDocument(); + const releaseStatus = screen.getByText('ready'); + expect(releaseStatus).toBeInTheDocument(); + expect(releaseStatus).toHaveStyle(`color: #328048`); + const cat1Row = screen.getByRole('row', { name: /cat1/i }); expect(within(cat1Row).getByRole('gridcell', { name: 'Ready to publish' })).toBeInTheDocument(); @@ -181,4 +188,31 @@ describe('Releases details page', () => { within(add1Row).getByRole('gridcell', { name: 'Already published' }) ).toBeInTheDocument(); }); + + it('shows the right release status for published release', async () => { + server.use( + rest.get('/content-releases/:releaseId', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withActionsAndPublishedHeaderData)) + ) + ); + + server.use( + rest.get('/content-releases/:releaseId/actions', (req, res, ctx) => + res(ctx.json(mockReleaseDetailsPageData.withMultipleActionsBodyData)) + ) + ); + + render(, { + initialEntries: [{ pathname: `/content-releases/3` }], + }); + + const releaseTitle = await screen.findByText( + mockReleaseDetailsPageData.withActionsAndPublishedHeaderData.data.name + ); + expect(releaseTitle).toBeInTheDocument(); + + const releaseStatus = screen.getByText('done'); + expect(releaseStatus).toBeInTheDocument(); + expect(releaseStatus).toHaveStyle(`color: #4945ff`); + }); }); diff --git a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts index 4a0c9d3bb0..7a61e30695 100644 --- a/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts +++ b/packages/core/content-releases/admin/src/pages/tests/mockReleaseDetailsPageData.ts @@ -9,6 +9,7 @@ const RELEASE_NO_ACTIONS_HEADER_MOCK_DATA = { createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: null, + status: 'empty', createdBy: { id: 1, firstname: 'Admin', @@ -50,6 +51,7 @@ const RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: null, + status: 'ready', createdBy: { id: 1, firstname: 'Admin', @@ -70,11 +72,12 @@ const RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { const PUBLISHED_RELEASE_WITH_ACTIONS_HEADER_MOCK_DATA = { data: { - id: 2, + id: 3, name: 'release with actions', createdAt: '2023-11-16T15:18:32.560Z', updatedAt: '2023-11-16T15:18:32.560Z', releasedAt: '2023-11-16T15:18:32.560Z', + status: 'done', createdBy: { id: 1, firstname: 'Admin', diff --git a/packages/core/helper-plugin/src/components/RelativeTime.tsx b/packages/core/helper-plugin/src/components/RelativeTime.tsx index 1f54585ac4..b57ecc74c8 100644 --- a/packages/core/helper-plugin/src/components/RelativeTime.tsx +++ b/packages/core/helper-plugin/src/components/RelativeTime.tsx @@ -12,6 +12,7 @@ interface CustomInterval { export interface RelativeTimeProps { timestamp: Date; customIntervals?: CustomInterval[]; + className?: string; } /** @@ -28,7 +29,7 @@ export interface RelativeTimeProps { * ]} * ``` */ -const RelativeTime = ({ timestamp, customIntervals = [] }: RelativeTimeProps) => { +const RelativeTime = ({ timestamp, customIntervals = [], className }: RelativeTimeProps) => { const { formatRelativeTime, formatDate, formatTime } = useIntl(); const interval = intervalToDuration({ @@ -54,6 +55,7 @@ const RelativeTime = ({ timestamp, customIntervals = [] }: RelativeTimeProps) =>