diff --git a/admin/src/components/Action/Action.tsx b/admin/src/components/Action/Action.tsx index ab2d102..5a7d33f 100644 --- a/admin/src/components/Action/Action.tsx +++ b/admin/src/components/Action/Action.tsx @@ -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); @@ -57,7 +58,6 @@ const Action = ({ mode, documentId, entitySlug, locale }) => { // Handlers function handleDateChange(date) { setExecuteAt(date); - //setExecuteAt(date.toISOString()); } const handleOnEdit = () => { diff --git a/server/middlewares/validate-before-scheduling.js b/server/middlewares/validate-before-scheduling.js index ff9b49f..313f0fd 100644 --- a/server/middlewares/validate-before-scheduling.js +++ b/server/middlewares/validate-before-scheduling.js @@ -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 diff --git a/server/services/publication-service.js b/server/services/publication-service.js index 840dbcc..b06258e 100644 --- a/server/services/publication-service.js +++ b/server/services/publication-service.js @@ -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(); @@ -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}"`)); } }, /** @@ -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}"`)); } }, /** @@ -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