feat(content-releases): removing scheduling future flag for stable release

This commit is contained in:
Fernando Chavez 2024-03-13 13:57:54 +01:00
parent 298c6fc616
commit 5cd7dce98d
11 changed files with 127 additions and 229 deletions

View File

@ -1,5 +1 @@
module.exports = ({ env }) => ({
future: {
contentReleasesScheduling: env.bool('STRAPI_FEATURES_FUTURE_RELEASES_SCHEDULING', false),
},
});
module.exports = ({ env }) => ({});

View File

@ -1,5 +1 @@
module.exports = ({ env }) => ({
future: {
contentReleasesScheduling: env.bool('STRAPI_FUTURE_CONTENT_RELEASES_SCHEDULING', false),
},
});
module.exports = ({ env }) => ({});

View File

@ -259,7 +259,6 @@ export const CMReleasesContainer = () => {
} = useCMEditViewDataManager();
const contentTypeUid = slug as Common.UID.ContentType;
const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling');
const canFetch = entryId != null && contentTypeUid != null;
const fetchParams = canFetch
? {
@ -361,7 +360,7 @@ export const CMReleasesContainer = () => {
<Typography fontSize={2} fontWeight="bold" variant="omega" textColor="neutral700">
{release.name}
</Typography>
{IsSchedulingEnabled && release.scheduledAt && release.timezone && (
{release.scheduledAt && release.timezone && (
<Typography variant="pi" textColor="neutral600">
{formatMessage(
{

View File

@ -51,7 +51,6 @@ export const ReleaseModal = ({
const { formatMessage } = useIntl();
const { pathname } = useLocation();
const isCreatingRelease = pathname === `/plugins/${pluginId}`;
const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling');
// Set default first timezone from the list if no system timezone detected
const { timezoneList, systemTimezone = { value: 'UTC+00:00-Africa/Abidjan ' } } = getTimezones(
initialValues.scheduledAt ? new Date(initialValues.scheduledAt) : new Date()
@ -122,97 +121,89 @@ export const ReleaseModal = ({
onChange={handleChange}
required
/>
{/* Remove future flag check after Scheduling Beta release */}
{IsSchedulingEnabled && (
<Box width="max-content">
<Checkbox
name="isScheduled"
value={values.isScheduled}
onChange={(event) => {
setFieldValue('isScheduled', event.target.checked);
if (!event.target.checked) {
// Clear scheduling info from a release on unchecking schedule release, which reset scheduling info in DB
setFieldValue('date', null);
setFieldValue('time', '');
setFieldValue('timezone', null);
} else {
// On ticking back schedule release date, time and timezone should be restored to the initial state
setFieldValue('date', initialValues.date);
setFieldValue('time', initialValues.time);
setFieldValue('timezone', initialValues.timezone ?? systemTimezone?.value);
}
}}
>
<Typography
textColor={values.isScheduled ? 'primary600' : 'neutral800'}
fontWeight={values.isScheduled ? 'semiBold' : 'regular'}
>
{formatMessage({
id: 'modal.form.input.label.schedule-release',
defaultMessage: 'Schedule release',
})}
</Typography>
</Checkbox>
</Box>
{values.isScheduled && (
<>
<Box width="max-content">
<Checkbox
name="isScheduled"
value={values.isScheduled}
onChange={(event) => {
setFieldValue('isScheduled', event.target.checked);
if (!event.target.checked) {
// Clear scheduling info from a release on unchecking schedule release, which reset scheduling info in DB
setFieldValue('date', null);
setFieldValue('time', '');
setFieldValue('timezone', null);
} else {
// On ticking back schedule release date, time and timezone should be restored to the initial state
setFieldValue('date', initialValues.date);
setFieldValue('time', initialValues.time);
setFieldValue(
'timezone',
initialValues.timezone ?? systemTimezone?.value
);
}
}}
>
<Typography
textColor={values.isScheduled ? 'primary600' : 'neutral800'}
fontWeight={values.isScheduled ? 'semiBold' : 'regular'}
>
{formatMessage({
id: 'modal.form.input.label.schedule-release',
defaultMessage: 'Schedule release',
<Flex gap={4} alignItems="start">
<Box width="100%">
<DatePicker
label={formatMessage({
id: 'content-releases.modal.form.input.label.date',
defaultMessage: 'Date',
})}
</Typography>
</Checkbox>
</Box>
{values.isScheduled && (
<>
<Flex gap={4} alignItems="start">
<Box width="100%">
<DatePicker
label={formatMessage({
id: 'content-releases.modal.form.input.label.date',
defaultMessage: 'Date',
})}
name="date"
error={errors.date}
onChange={(date) => {
const isoFormatDate = date
? formatISO(date, { representation: 'date' })
: null;
setFieldValue('date', isoFormatDate);
}}
clearLabel={formatMessage({
id: 'content-releases.modal.form.input.clearLabel',
defaultMessage: 'Clear',
})}
onClear={() => {
setFieldValue('date', null);
}}
selectedDate={values.date || undefined}
required
minDate={utcToZonedTime(new Date(), values.timezone.split('&')[1])}
/>
</Box>
<Box width="100%">
<TimePicker
label={formatMessage({
id: 'content-releases.modal.form.input.label.time',
defaultMessage: 'Time',
})}
name="time"
error={errors.time}
onChange={(time) => {
setFieldValue('time', time);
}}
clearLabel={formatMessage({
id: 'content-releases.modal.form.input.clearLabel',
defaultMessage: 'Clear',
})}
onClear={() => {
setFieldValue('time', '');
}}
value={values.time || undefined}
required
/>
</Box>
</Flex>
<TimezoneComponent timezoneOptions={timezoneList} />
</>
)}
name="date"
error={errors.date}
onChange={(date) => {
const isoFormatDate = date
? formatISO(date, { representation: 'date' })
: null;
setFieldValue('date', isoFormatDate);
}}
clearLabel={formatMessage({
id: 'content-releases.modal.form.input.clearLabel',
defaultMessage: 'Clear',
})}
onClear={() => {
setFieldValue('date', null);
}}
selectedDate={values.date || undefined}
required
minDate={utcToZonedTime(new Date(), values.timezone.split('&')[1])}
/>
</Box>
<Box width="100%">
<TimePicker
label={formatMessage({
id: 'content-releases.modal.form.input.label.time',
defaultMessage: 'Time',
})}
name="time"
error={errors.time}
onChange={(time) => {
setFieldValue('time', time);
}}
clearLabel={formatMessage({
id: 'content-releases.modal.form.input.clearLabel',
defaultMessage: 'Clear',
})}
onClear={() => {
setFieldValue('time', '');
}}
value={values.time || undefined}
required
/>
</Box>
</Flex>
<TimezoneComponent timezoneOptions={timezoneList} />
</>
)}
</Flex>

View File

@ -315,7 +315,6 @@ export const ReleaseDetailsLayout = ({
const totalEntries = release.actions.meta.count || 0;
const hasCreatedByUser = Boolean(getCreatedByUser());
const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling');
const isScheduled = release.scheduledAt && release.timezone;
const numberOfEntriesText = formatMessage(
{
@ -354,8 +353,7 @@ export const ReleaseDetailsLayout = ({
subtitle={
<Flex gap={2} lineHeight={6}>
<Typography textColor="neutral600" variant="epsilon">
{numberOfEntriesText +
(IsSchedulingEnabled && isScheduled ? ` - ${scheduledText}` : '')}
{numberOfEntriesText + (isScheduled ? ` - ${scheduledText}` : '')}
</Typography>
<Badge {...getBadgeProps(release.status)}>{release.status}</Badge>
</Flex>

View File

@ -96,7 +96,6 @@ const getBadgeProps = (status: Release['status']) => {
const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: ReleasesGridProps) => {
const { formatMessage } = useIntl();
const IsSchedulingEnabled = window.strapi.future.isEnabled('contentReleasesScheduling');
if (isError) {
return <AnErrorOccurred />;
@ -121,7 +120,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases
return (
<Grid gap={4}>
{releases.map(({ id, name, actions, scheduledAt, status }) => (
{releases.map(({ id, name, scheduledAt, status }) => (
<GridItem col={3} s={6} xs={12} key={id}>
<LinkCard href={`content-releases/${id}`} isExternal={false}>
<Flex
@ -141,24 +140,13 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases
{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',
})
)
{scheduledAt ? (
<CapitalizeRelativeTime timestamp={new Date(scheduledAt)} />
) : (
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.pages.Releases.not-scheduled',
defaultMessage: 'Not scheduled',
})
)}
</Typography>
</Flex>
@ -191,8 +179,7 @@ const INITIAL_FORM_VALUES = {
name: '',
date: null,
time: '',
// Remove future flag check after Scheduling Beta release and replace with true as creating new release should include scheduling by default
isScheduled: window.strapi.future.isEnabled('contentReleasesScheduling'),
isScheduled: true,
scheduledAt: null,
timezone: null,
} satisfies FormValues;

View File

@ -5,8 +5,6 @@ import { ACTIONS, RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constan
const { features } = require('@strapi/strapi/dist/utils/ee');
const { register } = require('../register');
const { bootstrap } = require('../bootstrap');
const { getService } = require('../utils');
jest.mock('@strapi/strapi/dist/utils/ee', () => ({
features: {
@ -99,57 +97,3 @@ describe('register', () => {
expect(mockGraphQlDisable).toHaveBeenCalledTimes(2);
});
});
describe('bootstrap', () => {
const mockSyncFromDatabase = jest.fn();
getService.mockReturnValue({
syncFromDatabase: mockSyncFromDatabase,
});
const strapi = {
db: {
lifecycles: {
subscribe: jest.fn(),
},
},
features: {
future: {
isEnabled: jest.fn(),
},
},
log: {
error: jest.fn(),
},
contentTypes: {
contentTypeA: {
uid: 'contentTypeA',
},
contentTypeB: {
uid: 'contentTypeB',
},
},
webhookStore: {
addAllowedEvent: jest.fn(),
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should sync scheduled jobs from the database if contentReleasesScheduling flag is enabled', async () => {
strapi.features.future.isEnabled.mockReturnValue(true);
features.isEnabled.mockReturnValue(true);
mockSyncFromDatabase.mockResolvedValue(new Map());
await bootstrap({ strapi });
expect(mockSyncFromDatabase).toHaveBeenCalled();
});
it('should not sync scheduled jobs from the database if contentReleasesScheduling flag is disabled', async () => {
strapi.features.future.isEnabled.mockReturnValue(false);
features.isEnabled.mockReturnValue(true);
await bootstrap({ strapi });
expect(mockSyncFromDatabase).not.toHaveBeenCalled();
});
});

View File

@ -153,20 +153,18 @@ export const bootstrap = async ({ strapi }: { strapi: LoadedStrapi }) => {
},
});
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
getService('scheduling', { strapi })
.syncFromDatabase()
.catch((err: Error) => {
strapi.log.error(
'Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling.'
);
getService('scheduling', { strapi })
.syncFromDatabase()
.catch((err: Error) => {
strapi.log.error(
'Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling.'
);
throw err;
});
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
throw err;
});
}
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
});
}
};

View File

@ -5,13 +5,11 @@ import { Release } from '../../shared/contracts/releases';
import { getService } from './utils';
export const destroy = async ({ strapi }: { strapi: LoadedStrapi }) => {
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
const scheduledJobs: Map<Release['id'], Job> = getService('scheduling', {
strapi,
}).getAll();
const scheduledJobs: Map<Release['id'], Job> = getService('scheduling', {
strapi,
}).getAll();
for (const [, job] of scheduledJobs) {
job.cancel();
}
for (const [, job] of scheduledJobs) {
job.cancel();
}
};

View File

@ -5,5 +5,5 @@ import scheduling from './scheduling';
export const services = {
release,
'release-validation': releaseValidation,
...(strapi.features.future.isEnabled('contentReleasesScheduling') ? { scheduling } : {}),
scheduling,
};

View File

@ -224,10 +224,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => {
},
});
if (
strapi.features.future.isEnabled('contentReleasesScheduling') &&
releaseWithCreatorFields.scheduledAt
) {
if (releaseWithCreatorFields.scheduledAt) {
const schedulingService = getService('scheduling', { strapi });
await schedulingService.set(release.id, release.scheduledAt);
@ -390,16 +387,14 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => {
data: releaseWithCreatorFields,
});
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
const schedulingService = getService('scheduling', { strapi });
const schedulingService = getService('scheduling', { strapi });
if (releaseData.scheduledAt) {
// set function always cancel the previous job if it exists, so we can call it directly
await schedulingService.set(id, releaseData.scheduledAt);
} else if (release.scheduledAt) {
// When user don't send a scheduledAt and we have one on the release, means that user want to unschedule it
schedulingService.cancel(id);
}
if (releaseData.scheduledAt) {
// set function always cancel the previous job if it exists, so we can call it directly
await schedulingService.set(id, releaseData.scheduledAt);
} else if (release.scheduledAt) {
// When user don't send a scheduledAt and we have one on the release, means that user want to unschedule it
schedulingService.cancel(id);
}
this.updateReleaseStatus(id);
@ -630,7 +625,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => {
await strapi.entityService.delete(RELEASE_MODEL_UID, releaseId);
});
if (strapi.features.future.isEnabled('contentReleasesScheduling') && release.scheduledAt) {
if (release.scheduledAt) {
const schedulingService = getService('scheduling', { strapi });
await schedulingService.cancel(release.id);
}
@ -706,23 +701,19 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => {
},
});
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: true,
release,
});
}
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: true,
release,
});
strapi.telemetry.send('didPublishContentRelease');
return { release, error: null };
} catch (error) {
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: false,
error,
});
}
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: false,
error,
});
// We need to run the update in the same transaction because the release is locked
await strapi.db