Feat(content-releases): release status badge (#19611)

* feat(content-releases): add status to releases

* add docs and fix e2e error

* draft: added basic badge with default value

* Update docs/docs/docs/01-core/content-releases/00-intro.md

Co-authored-by: Simone <startae14@gmail.com>

* Update docs/docs/docs/01-core/content-releases/00-intro.md

Co-authored-by: Simone <startae14@gmail.com>

* Update docs/docs/docs/01-core/content-releases/00-intro.md

Co-authored-by: Simone <startae14@gmail.com>

* apply marks feedback

* don't throw error on lifecycle hooks inside releases

* handle when actions are not valid anymore

* await for entry validation on releases edit entry

* check if are changes in content types attributes to revalidate

* fix e2e test

* apply marks feedback

* fix: removed default status value

* fix: release card design updated, capitalize scheduled period

* fix: e2e test updated to select always a current date

---------

Co-authored-by: Fernando Chavez <fernando.chavez@strapi.io>
Co-authored-by: Simone <startae14@gmail.com>
This commit is contained in:
Madhuri Sandbhor 2024-03-04 11:50:38 +01:00 committed by GitHub
parent 73143c2805
commit ad6d81cb6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 33 deletions

View File

@ -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', {

View File

@ -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 = ({
<HeaderLayout
title={release.name}
subtitle={
numberOfEntriesText + (IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : '')
<Flex gap={2} lineHeight={6}>
<Typography textColor="neutral600" variant="epsilon">
{numberOfEntriesText +
(IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : '')}
</Typography>
<Badge {...getBadgeProps(release.status)}>{release.status}</Badge>
</Flex>
}
navigationAction={
<Link startIcon={<ArrowLeft />} to="/plugins/content-releases">

View File

@ -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 (
<Grid gap={4}>
{releases.map(({ id, name, actions, scheduledAt }) => (
{releases.map(({ id, name, actions, scheduledAt, status }) => (
<GridItem col={3} s={6} xs={12} key={id}>
<LinkCard href={`content-releases/${id}`} isExternal={false}>
<Flex
@ -102,32 +134,35 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases
height="100%"
width="100%"
alignItems="start"
gap={2}
gap={4}
>
<Typography as="h3" variant="delta" fontWeight="bold">
{name}
</Typography>
<Typography variant="pi" textColor="neutral600">
{IsSchedulingEnabled ? (
scheduledAt ? (
<RelativeTime timestamp={new Date(scheduledAt)} />
<Flex direction="column" alignItems="start" gap={1}>
<Typography as="h3" variant="delta" fontWeight="bold">
{name}
</Typography>
<Typography variant="pi" textColor="neutral600">
{IsSchedulingEnabled ? (
scheduledAt ? (
<CapitalizeRelativeTime timestamp={new Date(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 }
)
)}
</Typography>
formatMessage(
{
id: 'content-releases.page.Releases.release-item.entries',
defaultMessage:
'{number, plural, =0 {No entries} one {# entry} other {# entries}}',
},
{ number: actions.meta.count }
)
)}
</Typography>
</Flex>
<Badge {...getBadgeProps(status)}>{status}</Badge>
</Flex>
</LinkCard>
</GridItem>
@ -405,4 +440,4 @@ const ReleasesPage = () => {
);
};
export { ReleasesPage };
export { ReleasesPage, getBadgeProps };

View File

@ -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(<ReleaseDetailsPage />, {
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(<ReleaseDetailsPage />, {
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`);
});
});

View File

@ -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',

View File

@ -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) =>
<time
dateTime={timestamp.toISOString()}
title={`${formatDate(timestamp)} ${formatTime(timestamp)}`}
className={className}
>
{displayText}
</time>