Skip to content

Commit 3c19618

Browse files
authored
Fix for middleware check + fine tune locale (#211)
This fix will resolve: -Update locales -Update middleware for validation errors #206 #207 #210
1 parent c261124 commit 3c19618

3 files changed

Lines changed: 154 additions & 31 deletions

File tree

admin/src/components/Action/Action.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const Action = ({ mode, documentId, entitySlug, locale }) => {
4646
setIsLoading(false);
4747
if (data) {
4848
setActionId(data.documentId);
49-
setExecuteAt(data.executeAt);
49+
// Convert UTC date from server to local Date object for DateTimePicker
50+
setExecuteAt(data.executeAt ? new Date(data.executeAt) : null);
5051
setIsEditing(true);
5152
} else {
5253
setActionId(0);
@@ -57,7 +58,6 @@ const Action = ({ mode, documentId, entitySlug, locale }) => {
5758
// Handlers
5859
function handleDateChange(date) {
5960
setExecuteAt(date);
60-
//setExecuteAt(date.toISOString());
6161
}
6262

6363
const handleOnEdit = () => {

server/middlewares/validate-before-scheduling.js

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@ import { errors } from '@strapi/utils';
33
const validationMiddleware = async (context, next) => {
44
const { uid, action, params } = context;
55
// Run this middleware only for the publisher action.
6-
if (uid !== 'plugin::publisher.action') {
7-
return next();
8-
}
6+
if (uid !== 'plugin::publisher.action') return next();
97

108
// Run it only for the create and update actions.
11-
if (action !== 'create' && action !== 'update') {
12-
return next();
13-
}
9+
if (action !== 'create' && action !== 'update') return next();
1410

1511
// The create action will have the data directly.
16-
let publisherAction = params.data;
12+
let publisherAction = params?.data;
1713

1814
// The update action might have incomplete data, so we need to fetch it.
1915
if (action === 'update') {
@@ -25,13 +21,11 @@ const validationMiddleware = async (context, next) => {
2521
// The complete, and possibly updated, publisher action.
2622
const { entityId, entitySlug, mode, locale: actionLocale } = {
2723
...publisherAction,
28-
...params.data,
24+
...params?.data,
2925
};
3026

3127
// Run it only for the publish mode.
32-
if (mode !== 'publish') {
33-
return next();
34-
}
28+
if (mode !== 'publish') return next();
3529

3630
const populateBuilderService = strapi.plugin('content-manager').service('populate-builder');
3731
const populate = await populateBuilderService(entitySlug).populateDeep(Infinity).build();
@@ -46,28 +40,136 @@ const validationMiddleware = async (context, next) => {
4640

4741
if (!draft) {
4842
throw new errors.NotFoundError(
49-
`No draft found for ${entitySlug} with documentId "${entityId}"${actionLocale ? ` and locale "${actionLocale}".` : '.'}`
43+
`No draft found for ${entitySlug} with documentId "${entityId}"${
44+
actionLocale ? ` and locale "${actionLocale}".` : '.'
45+
}`
5046
);
5147
}
5248

53-
// If no locale was provided in params.data, fill it in from the draft
49+
// If no locale was provided in params.data, fill it in from the draft.
5450
const locale = actionLocale || draft.locale;
5551

56-
// Fetch the published entity in this same locale
52+
// Fetch the published entity in this same locale.
5753
const published = await strapi.documents(entitySlug).findOne({
5854
documentId: entityId,
5955
status: 'published',
6056
locale,
6157
populate,
6258
});
6359

64-
// Validate the draft before scheduling the publication.
65-
await strapi.entityValidator.validateEntityCreation(
66-
strapi.contentType(entitySlug),
67-
draft,
68-
{ isDraft: false, locale },
69-
published
70-
);
60+
const model = strapi.contentType(entitySlug);
61+
62+
// ---------- helpers ----------
63+
const isEmptyValue = (value, { multiple, repeatable }) => {
64+
if (multiple || repeatable) return !Array.isArray(value) || value.length === 0;
65+
return value === null || value === undefined;
66+
};
67+
68+
// Minimal custom check: only required media/relations + nested structure inside components/DZ.
69+
const collectRequiredMissing = (schema, dataNode, pathArr = []) => {
70+
const errs = [];
71+
const attrs = schema?.attributes || {};
72+
73+
for (const [name, attr] of Object.entries(attrs)) {
74+
const nextPath = [...pathArr, name];
75+
const value = dataNode ? dataNode[name] : undefined;
76+
77+
// Media fields
78+
if (attr.type === 'media') {
79+
if (attr.required && isEmptyValue(value, { multiple: !!attr.multiple })) {
80+
errs.push({ path: nextPath, message: 'This field is required' });
81+
}
82+
continue;
83+
}
84+
85+
// Relations
86+
if (attr.type === 'relation') {
87+
const many =
88+
['oneToMany', 'manyToMany', 'morphToMany'].includes(attr.relation) ||
89+
(typeof attr.relation === 'string' && attr.relation.toLowerCase().includes('many'));
90+
if (attr.required && isEmptyValue(value, { multiple: many })) {
91+
errs.push({ path: nextPath, message: 'This field is required' });
92+
}
93+
continue;
94+
}
95+
96+
// Components (repeatable or single)
97+
if (attr.type === 'component') {
98+
if (attr.required && isEmptyValue(value, { repeatable: !!attr.repeatable })) {
99+
errs.push({ path: nextPath, message: 'This field is required' });
100+
continue;
101+
}
102+
const compSchema = strapi.components[attr.component];
103+
if (attr.repeatable && Array.isArray(value)) {
104+
value.forEach((item, idx) => {
105+
errs.push(...collectRequiredMissing(compSchema, item, [...nextPath, idx]));
106+
});
107+
} else if (value) {
108+
errs.push(...collectRequiredMissing(compSchema, value, nextPath));
109+
}
110+
continue;
111+
}
112+
113+
// Dynamic zones
114+
if (attr.type === 'dynamiczone') {
115+
if (attr.required && (!Array.isArray(value) || value.length === 0)) {
116+
errs.push({ path: nextPath, message: 'This field is required' });
117+
continue;
118+
}
119+
if (Array.isArray(value)) {
120+
value.forEach((dzItem, idx) => {
121+
const compUid = dzItem?.__component;
122+
if (!compUid) return;
123+
const compSchema = strapi.components[compUid];
124+
errs.push(...collectRequiredMissing(compSchema, dzItem, [...nextPath, idx]));
125+
});
126+
}
127+
continue;
128+
}
129+
}
130+
131+
return errs;
132+
};
133+
134+
// ---------- run core validator, normalize, and (optionally) add extras ----------
135+
try {
136+
await strapi.entityValidator.validateEntityCreation(
137+
model,
138+
draft,
139+
{ isDraft: false, locale },
140+
published
141+
);
142+
} catch (e) {
143+
const name = e?.name || e?.constructor?.name;
144+
const isValidationLike =
145+
Array.isArray(e?.details?.errors) || /ValidationError/i.test(name || '');
146+
147+
if (isValidationLike) {
148+
// Use core errors and supplement with missing media/relations if needed.
149+
const core = (e.details?.errors || []).map((er) => ({
150+
path: er.path || er.name || '',
151+
message: er.message || 'This field is required',
152+
}));
153+
const extras = collectRequiredMissing(model, draft);
154+
const merged = [...core, ...extras];
155+
156+
throw new errors.ValidationError(
157+
'There are validation errors in your document. Please fix them so you can publish.',
158+
{ errors: merged }
159+
);
160+
}
161+
162+
throw e;
163+
}
164+
165+
// Enforce required media/relations even if core validator passed
166+
const extrasAfterPass = collectRequiredMissing(model, draft);
167+
if (extrasAfterPass.length > 0) {
168+
throw new errors.ValidationError(
169+
'There are validation errors in your document. Please fix them so you can publish.',
170+
{ errors: extrasAfterPass }
171+
);
172+
}
71173

72174
return next();
73175
};

server/services/publication-service.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default ({ strapi }) => ({
88
* Publish a single record
99
*
1010
*/
11-
async publish(uid, entityId, { locale }) {
11+
async publish(uid, entityId, { locale, publishedAt }) {
1212
try {
1313
const { hooks } = getPluginService('settingsService').get();
1414

@@ -25,18 +25,39 @@ export default ({ strapi }) => ({
2525
return;
2626
}
2727

28-
const publishedEntity = await strapi.documents(uid).publish({
28+
let publishedEntity = await strapi.documents(uid).publish({
2929
documentId: entityId,
3030
locale,
3131
});
3232

33+
// If a custom publishedAt is provided, update it directly via the database layer
34+
if (publishedAt) {
35+
// Get the internal ID of the published entry
36+
const publishedRecord = publishedEntity.entries?.[0];
37+
38+
if (publishedRecord?.id) {
39+
// Use db.query to directly update the publishedAt field in the database
40+
await strapi.db.query(uid).update({
41+
where: { id: publishedRecord.id },
42+
data: { publishedAt },
43+
});
44+
45+
// Fetch the updated entity to return the correct data
46+
publishedEntity = await strapi.documents(uid).findOne({
47+
documentId: entityId,
48+
locale,
49+
status: 'published',
50+
});
51+
}
52+
}
53+
3354
await getPluginService('emitService').publish(uid, publishedEntity);
3455

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

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

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

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

7293
await hooks.afterUnpublish({ strapi, uid, entity: unpublishedEntity });
7394
} catch (error) {
74-
strapi.log.error(logMessage(`An error occurred when trying to unpublish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
95+
strapi.log.error(logMessage(`An error occurred when trying to unpublish document with id "${entityId}"${locale ? ` and locale "${locale}"` : ''} of type "${uid}": "${error}"`));
7596
}
7697
},
7798
/**
@@ -85,14 +106,14 @@ export default ({ strapi }) => ({
85106
const publishedEntity = await strapi.documents(record.entitySlug).findOne({
86107
documentId: entityId,
87108
status: 'published',
88-
locale: record.locale
109+
...(record.locale ? { locale: record.locale } : {}),
89110
});
90111

91112
// Find the draft version of the entity
92113
const draftEntity = await strapi.documents(record.entitySlug).findOne({
93114
documentId: entityId,
94115
status: 'draft',
95-
locale: record.locale
116+
...(record.locale ? { locale: record.locale } : {}),
96117
});
97118

98119
// Determine the current state of the entity

0 commit comments

Comments
 (0)