Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions admin/src/components/Action/Action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ const Action = ({ mode, documentId, entitySlug, locale }) => {
setIsLoading(false);
if (data) {
setActionId(data.documentId);
setExecuteAt(data.executeAt);
// Convert UTC date from server to local Date object for DateTimePicker
setExecuteAt(data.executeAt ? new Date(data.executeAt) : null);
setIsEditing(true);
} else {
setActionId(0);
Expand All @@ -57,7 +58,6 @@ const Action = ({ mode, documentId, entitySlug, locale }) => {
// Handlers
function handleDateChange(date) {
setExecuteAt(date);
//setExecuteAt(date.toISOString());
}

const handleOnEdit = () => {
Expand Down
94 changes: 92 additions & 2 deletions server/middlewares/validate-before-scheduling.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,99 @@ const validationMiddleware = async (context, next) => {
populate,
});

// Validate the draft before scheduling the publication.
// -------------------------
// Extra validation for media and relations
// -------------------------
const isEmptyValue = (value, { multiple, repeatable }) => {
if (multiple || repeatable) {
return !Array.isArray(value) || value.length === 0;
}
return value === null || value === undefined;
};

const contentType = strapi.contentType(entitySlug);

// Recursively validate required relations
const validateRequiredRelations = (schema, dataNode, path = '') => {
const errs = [];
const attrs = schema.attributes || {};

for (const [name, attr] of Object.entries(attrs)) {
const currentPath = path ? `${path}.${name}` : name;
const value = dataNode ? dataNode[name] : undefined;

// media
if (attr.type === 'media') {
if (attr.required && isEmptyValue(value, { multiple: !!attr.multiple })) {
errs.push(`Field "${currentPath}" (media) is required`);
}
continue;
}

// relation
if (attr.type === 'relation') {
if (attr.required && isEmptyValue(value, { multiple: attr.relation === 'oneToMany' || attr.relation === 'manyToMany' || attr.relation === 'morphToMany' })) {
errs.push(`Field "${currentPath}" (relation) is required`);
}
continue;
}

// component
if (attr.type === 'component') {
if (attr.required && isEmptyValue(value, { repeatable: !!attr.repeatable })) {
errs.push(`Field "${currentPath}" (component${attr.repeatable ? '[]' : ''}) is required`);
continue;
}
// Recurse into component(s)
const componentSchema = strapi.components[attr.component];
if (attr.repeatable) {
if (Array.isArray(value)) {
value.forEach((item, idx) => {
errs.push(
...validateRequiredRelations(componentSchema, item, `${currentPath}[${idx}]`)
);
});
}
} else if (value) {
errs.push(
...validateRequiredRelations(componentSchema, value, currentPath)
);
}
continue;
}

// dynamic zone
if (attr.type === 'dynamiczone') {
if (attr.required && (!Array.isArray(value) || value.length === 0)) {
errs.push(`Field "${currentPath}" (dynamic zone) is required`);
continue;
}
if (Array.isArray(value)) {
value.forEach((dzItem, idx) => {
const compUid = dzItem && dzItem.__component;
if (!compUid) return;
const compSchema = strapi.components[compUid];
errs.push(
...validateRequiredRelations(compSchema, dzItem, `${currentPath}[${idx}]`)
);
});
}
continue;
}
}

return errs;
};

const relationErrors = validateRequiredRelations(contentType, draft);
if (relationErrors.length > 0) {
throw new errors.ValidationError(
`Cannot schedule publish: missing required relation/media fields.\n` +
relationErrors.map((e) => `- ${e}`).join('\n')
);
}
await strapi.entityValidator.validateEntityCreation(
strapi.contentType(entitySlug),
contentType,
draft,
{ isDraft: false, locale },
published
Expand Down
37 changes: 29 additions & 8 deletions server/services/publication-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default ({ strapi }) => ({
* Publish a single record
*
*/
async publish(uid, entityId, { locale }) {
async publish(uid, entityId, { locale, publishedAt }) {
try {
const { hooks } = getPluginService('settingsService').get();

Expand All @@ -25,18 +25,39 @@ export default ({ strapi }) => ({
return;
}

const publishedEntity = await strapi.documents(uid).publish({
let publishedEntity = await strapi.documents(uid).publish({
documentId: entityId,
locale,
});

// If a custom publishedAt is provided, update it directly via the database layer
if (publishedAt) {
// Get the internal ID of the published entry
const publishedRecord = publishedEntity.entries?.[0];

if (publishedRecord?.id) {
// Use db.query to directly update the publishedAt field in the database
await strapi.db.query(uid).update({
where: { id: publishedRecord.id },
data: { publishedAt },
});

// Fetch the updated entity to return the correct data
publishedEntity = await strapi.documents(uid).findOne({
documentId: entityId,
locale,
status: 'published',
});
}
}

await getPluginService('emitService').publish(uid, publishedEntity);

strapi.log.info(logMessage(`Successfully published document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}".`));
strapi.log.info(logMessage(`Successfully published document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}".`));

await hooks.afterPublish({ strapi, uid, entity: publishedEntity });
} catch (error) {
strapi.log.error(logMessage(`An error occurred when trying to publish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
strapi.log.error(logMessage(`An error occurred when trying to publish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
}
},
/**
Expand Down Expand Up @@ -67,11 +88,11 @@ export default ({ strapi }) => ({

await getPluginService('emitService').unpublish(uid, unpublishedEntity);

strapi.log.info(logMessage(`Successfully unpublished document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}".`));
strapi.log.info(logMessage(`Successfully unpublished document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}".`));

await hooks.afterUnpublish({ strapi, uid, entity: unpublishedEntity });
} catch (error) {
strapi.log.error(logMessage(`An error occurred when trying to unpublish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
strapi.log.error(logMessage(`An error occurred when trying to unpublish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
}
},
/**
Expand All @@ -85,14 +106,14 @@ export default ({ strapi }) => ({
const publishedEntity = await strapi.documents(record.entitySlug).findOne({
documentId: entityId,
status: 'published',
locale: record.locale
...(record.locale ? { locale: record.locale } : {}),
});

// Find the draft version of the entity
const draftEntity = await strapi.documents(record.entitySlug).findOne({
documentId: entityId,
status: 'draft',
locale: record.locale
...(record.locale ? { locale: record.locale } : {}),
});

// Determine the current state of the entity
Expand Down