@raketa-cloud/admin 1.10.7 → 1.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/resources/PartialTranslationsHandler.js +85 -16
- package/src/tests/handlers/resources/PartialTranslationsHandler.test.js +25 -0
- package/src/tests/views/partial_translations.test.js +11 -0
- package/src/views/resource_revisions/_nav.ejs +1 -1
- package/src/views/resources/partial-translations/create_form.ejs +4 -3
- package/src/views/settings/_nav.ejs +1 -1
package/package.json
CHANGED
|
@@ -22,6 +22,39 @@ const targetTranslationLocales = (resource, sourceLocale) =>
|
|
|
22
22
|
)
|
|
23
23
|
.map(([locale]) => locale);
|
|
24
24
|
|
|
25
|
+
const localesAtOpenRevisionLimit = (resource, sourceLocale, maxOpenRevisions) =>
|
|
26
|
+
Object.entries(resource.translations || {})
|
|
27
|
+
.filter(
|
|
28
|
+
([locale, translation]) =>
|
|
29
|
+
locale !== sourceLocale &&
|
|
30
|
+
translation?.id &&
|
|
31
|
+
translation.revisionsCount >= maxOpenRevisions,
|
|
32
|
+
)
|
|
33
|
+
.map(([locale]) => locale);
|
|
34
|
+
|
|
35
|
+
const withTranslationRevisionCounts = async (ctx, resource, resourceType) => {
|
|
36
|
+
const revisionCounts = await Promise.all(
|
|
37
|
+
Object.entries(resource.translations || {}).map(
|
|
38
|
+
async ([locale, translation]) => {
|
|
39
|
+
if (locale === ctx.locale || !translation?.id) {
|
|
40
|
+
return [locale, translation];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const target = await ctx.client.resources.find(
|
|
44
|
+
translation.id,
|
|
45
|
+
locale,
|
|
46
|
+
resourceType,
|
|
47
|
+
);
|
|
48
|
+
return [
|
|
49
|
+
locale,
|
|
50
|
+
{ ...translation, revisionsCount: target.revisionsCount },
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return { ...resource, translations: Object.fromEntries(revisionCounts) };
|
|
57
|
+
};
|
|
25
58
|
const selectedWidgetIds = (resource, query = {}) => {
|
|
26
59
|
const requested = Array.isArray(query.widget_ids)
|
|
27
60
|
? query.widget_ids
|
|
@@ -73,6 +106,7 @@ const validatePartialTranslation = ({
|
|
|
73
106
|
workflow,
|
|
74
107
|
unknownWidgetIds,
|
|
75
108
|
unknownLocales,
|
|
109
|
+
localesAtRevisionLimit,
|
|
76
110
|
}) => {
|
|
77
111
|
const errors = [];
|
|
78
112
|
|
|
@@ -91,6 +125,12 @@ const validatePartialTranslation = ({
|
|
|
91
125
|
message: `not found in the resource translations: ${unknownLocales.join(", ")}`,
|
|
92
126
|
});
|
|
93
127
|
}
|
|
128
|
+
if (localesAtRevisionLimit.length > 0) {
|
|
129
|
+
errors.push({
|
|
130
|
+
key: "locales",
|
|
131
|
+
message: `open revision limit reached for: ${localesAtRevisionLimit.join(", ")}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
94
134
|
if (!workflow.type) {
|
|
95
135
|
errors.push({ key: "workflow", message: "can't be blank" });
|
|
96
136
|
}
|
|
@@ -131,14 +171,19 @@ const create_form = async (ctx) => {
|
|
|
131
171
|
throw new Error(`Cannot find ${resourceType} with id ${parent_id}`);
|
|
132
172
|
}
|
|
133
173
|
|
|
134
|
-
const
|
|
174
|
+
const resourceWithTranslationRevisionCounts =
|
|
175
|
+
await withTranslationRevisionCounts(ctx, resource, resourceType);
|
|
176
|
+
const widgetIds = selectedWidgetIds(
|
|
177
|
+
resourceWithTranslationRevisionCounts,
|
|
178
|
+
ctx.query,
|
|
179
|
+
);
|
|
135
180
|
const { validWidgetIds, unknownWidgetIds } = partitionWidgetIds(
|
|
136
|
-
|
|
181
|
+
resourceWithTranslationRevisionCounts,
|
|
137
182
|
widgetIds,
|
|
138
183
|
);
|
|
139
184
|
|
|
140
185
|
const previewUrl = ctx.client.resources.previewUrl(
|
|
141
|
-
{ ...
|
|
186
|
+
{ ...resourceWithTranslationRevisionCounts, resourceType },
|
|
142
187
|
ctx.locale,
|
|
143
188
|
);
|
|
144
189
|
const validationErrors = [];
|
|
@@ -170,7 +215,7 @@ const create_form = async (ctx) => {
|
|
|
170
215
|
if (validationErrors.length > 0) {
|
|
171
216
|
return {
|
|
172
217
|
assigns: formAssigns(
|
|
173
|
-
|
|
218
|
+
resourceWithTranslationRevisionCounts,
|
|
174
219
|
ctx.query,
|
|
175
220
|
previewUrl,
|
|
176
221
|
{ widgetIds: validWidgetIds },
|
|
@@ -182,7 +227,13 @@ const create_form = async (ctx) => {
|
|
|
182
227
|
};
|
|
183
228
|
}
|
|
184
229
|
|
|
185
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
assigns: formAssigns(
|
|
232
|
+
resourceWithTranslationRevisionCounts,
|
|
233
|
+
ctx.query,
|
|
234
|
+
previewUrl,
|
|
235
|
+
),
|
|
236
|
+
};
|
|
186
237
|
};
|
|
187
238
|
|
|
188
239
|
const create = async (ctx) => {
|
|
@@ -198,8 +249,10 @@ const create = async (ctx) => {
|
|
|
198
249
|
throw new Error(`Cannot find ${resourceType} with id ${parent_id}`);
|
|
199
250
|
}
|
|
200
251
|
|
|
252
|
+
const resourceWithTranslationRevisionCounts =
|
|
253
|
+
await withTranslationRevisionCounts(ctx, resource, resourceType);
|
|
201
254
|
const previewUrl = ctx.client.resources.previewUrl(
|
|
202
|
-
{ ...
|
|
255
|
+
{ ...resourceWithTranslationRevisionCounts, resourceType },
|
|
203
256
|
ctx.locale,
|
|
204
257
|
);
|
|
205
258
|
const values = ctx.resourceParams?.partial_translation || {};
|
|
@@ -210,30 +263,42 @@ const create = async (ctx) => {
|
|
|
210
263
|
: toArray(values.widget_ids);
|
|
211
264
|
const workflow = values.workflow || {};
|
|
212
265
|
const { validWidgetIds, unknownWidgetIds } = partitionWidgetIds(
|
|
213
|
-
|
|
266
|
+
resourceWithTranslationRevisionCounts,
|
|
214
267
|
widgetIds,
|
|
215
268
|
);
|
|
216
|
-
const validLocales = targetTranslationLocales(
|
|
269
|
+
const validLocales = targetTranslationLocales(
|
|
270
|
+
resourceWithTranslationRevisionCounts,
|
|
271
|
+
ctx.locale,
|
|
272
|
+
);
|
|
217
273
|
const unknownLocales = locales.filter(
|
|
218
274
|
(locale) => !validLocales.includes(locale),
|
|
219
275
|
);
|
|
220
|
-
const
|
|
221
|
-
|
|
276
|
+
const localesAtRevisionLimit = localesAtOpenRevisionLimit(
|
|
277
|
+
resourceWithTranslationRevisionCounts,
|
|
278
|
+
ctx.locale,
|
|
279
|
+
ctx.maxOpenRevisions,
|
|
280
|
+
);
|
|
281
|
+
const selectedValidLocales = locales.filter(
|
|
282
|
+
(locale) =>
|
|
283
|
+
validLocales.includes(locale) && !localesAtRevisionLimit.includes(locale),
|
|
222
284
|
);
|
|
223
285
|
|
|
224
286
|
const validationErrors = validatePartialTranslation({
|
|
225
|
-
resource,
|
|
287
|
+
resource: resourceWithTranslationRevisionCounts,
|
|
226
288
|
locales,
|
|
227
289
|
widgetIds,
|
|
228
290
|
workflow,
|
|
229
291
|
unknownWidgetIds,
|
|
230
292
|
unknownLocales,
|
|
293
|
+
localesAtRevisionLimit: locales.filter((locale) =>
|
|
294
|
+
localesAtRevisionLimit.includes(locale),
|
|
295
|
+
),
|
|
231
296
|
});
|
|
232
297
|
|
|
233
298
|
if (validationErrors.length > 0) {
|
|
234
299
|
return {
|
|
235
300
|
assigns: formAssigns(
|
|
236
|
-
|
|
301
|
+
resourceWithTranslationRevisionCounts,
|
|
237
302
|
ctx.query,
|
|
238
303
|
previewUrl,
|
|
239
304
|
{
|
|
@@ -250,21 +315,25 @@ const create = async (ctx) => {
|
|
|
250
315
|
};
|
|
251
316
|
}
|
|
252
317
|
|
|
253
|
-
const widgets = (
|
|
254
|
-
validWidgetIds.includes(widget.widgetId),
|
|
318
|
+
const widgets = (resourceWithTranslationRevisionCounts.widgets || []).filter(
|
|
319
|
+
(widget) => validWidgetIds.includes(widget.widgetId),
|
|
255
320
|
);
|
|
256
321
|
const { errors } = await ctx.client.resources.multiPartialTranslate(
|
|
257
322
|
parent_id,
|
|
258
323
|
locales,
|
|
259
324
|
widgets,
|
|
260
325
|
workflow,
|
|
261
|
-
{
|
|
326
|
+
{
|
|
327
|
+
...resourceWithTranslationRevisionCounts,
|
|
328
|
+
locale: ctx.locale,
|
|
329
|
+
resourceType,
|
|
330
|
+
},
|
|
262
331
|
);
|
|
263
332
|
|
|
264
333
|
if (errors) {
|
|
265
334
|
return {
|
|
266
335
|
assigns: formAssigns(
|
|
267
|
-
|
|
336
|
+
resourceWithTranslationRevisionCounts,
|
|
268
337
|
ctx.query,
|
|
269
338
|
previewUrl,
|
|
270
339
|
{ ...values, locales, widgetIds: validWidgetIds, workflow },
|
|
@@ -24,6 +24,7 @@ const buildCtx = (params = {}) => ({
|
|
|
24
24
|
params: { parent_id: "42" },
|
|
25
25
|
query: {},
|
|
26
26
|
locale: "en",
|
|
27
|
+
maxOpenRevisions: 1,
|
|
27
28
|
resourceConfig: { resourceType: "page", resourceTypePlural: "pages" },
|
|
28
29
|
resourceParams: params,
|
|
29
30
|
});
|
|
@@ -194,6 +195,30 @@ describe("PartialTranslationsHandler", () => {
|
|
|
194
195
|
});
|
|
195
196
|
});
|
|
196
197
|
|
|
198
|
+
test("does not submit to locales at the open revision limit", async () => {
|
|
199
|
+
const ctx = buildCtx({
|
|
200
|
+
partial_translation: {
|
|
201
|
+
locales: ["fr"],
|
|
202
|
+
widget_ids: ["hero-1"],
|
|
203
|
+
workflow: { type: "LOKALISE_TT_AI" },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
ctx.client.resources.find
|
|
207
|
+
.mockResolvedValueOnce({
|
|
208
|
+
...resource,
|
|
209
|
+
translations: { fr: { id: "43" } },
|
|
210
|
+
})
|
|
211
|
+
.mockResolvedValueOnce({ id: "43", revisionsCount: 1 });
|
|
212
|
+
|
|
213
|
+
const result = await PartialTranslationsHandler.create(ctx);
|
|
214
|
+
|
|
215
|
+
expect(ctx.client.resources.multiPartialTranslate).not.toHaveBeenCalled();
|
|
216
|
+
expect(result.assigns.partialTranslation.locales).toEqual([]);
|
|
217
|
+
expect(result.assigns.errors.partialTranslation).toEqual({
|
|
218
|
+
locales: "open revision limit reached for: fr",
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
197
222
|
test("does not submit when a translation has an active content task", async () => {
|
|
198
223
|
const ctx = buildCtx({
|
|
199
224
|
partial_translation: {
|
|
@@ -10,6 +10,7 @@ function renderPartialTranslations(translations) {
|
|
|
10
10
|
return ejs.render(template, {
|
|
11
11
|
_context: {
|
|
12
12
|
locale: "en",
|
|
13
|
+
maxOpenRevisions: 1,
|
|
13
14
|
locales: { en: { name: "English" }, fr: { name: "French" } },
|
|
14
15
|
resourceConfig: { resourceTypePlural: "pages" },
|
|
15
16
|
},
|
|
@@ -47,4 +48,14 @@ describe("partial translations form", () => {
|
|
|
47
48
|
expect(rendered).not.toContain('value="de"');
|
|
48
49
|
expect(rendered).not.toContain("This resource has no translations.");
|
|
49
50
|
});
|
|
51
|
+
|
|
52
|
+
test("disables translations at the open revision limit", () => {
|
|
53
|
+
const rendered = renderPartialTranslations({
|
|
54
|
+
fr: { id: "43", revisionsCount: 1 },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(rendered).toContain('value="fr"');
|
|
58
|
+
expect(rendered).toContain('value="fr" disabled');
|
|
59
|
+
expect(rendered).toContain("open revision limit reached");
|
|
60
|
+
});
|
|
50
61
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
<div class="flex gap-4 w-full pb-4">
|
|
11
11
|
<div class="flex items-center justify-between mb-4">
|
|
12
|
-
<div class="flex items-center border-b-2">
|
|
12
|
+
<div class="flex items-center border-b-2 border-neutral-200 dark:border-neutral-700">
|
|
13
13
|
<div class=" <%= active === 'general' ? activeClass : inactiveClass %>">
|
|
14
14
|
<a href="<%= `/${_context.locale}/${_context.resourceConfig.resourceType}_revisions/${resource.id}/general/edit` %>" class="text-raketa-primary font-semibold">
|
|
15
15
|
General
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
<div class="mb-4">
|
|
12
12
|
<h6>Current translations</h6>
|
|
13
13
|
<div class="checkbox-group !max-h-fit">
|
|
14
|
-
<% targetTranslations.forEach(([locale]) => { %>
|
|
15
|
-
<label>
|
|
16
|
-
<input type="checkbox" name="partial_translation[locales][]" value="<%= locale %>" <%= partialTranslation.locales.includes(locale) ? 'checked' : '' %>>
|
|
14
|
+
<% targetTranslations.forEach(([locale, translation]) => { const atRevisionLimit = translation.revisionsCount >= _context.maxOpenRevisions; %>
|
|
15
|
+
<label class="<%= atRevisionLimit ? 'opacity-50' : '' %>">
|
|
16
|
+
<input type="checkbox" name="partial_translation[locales][]" value="<%= locale %>" <%= partialTranslation.locales.includes(locale) ? 'checked' : '' %> <%= atRevisionLimit ? 'disabled' : '' %>>
|
|
17
17
|
<%= _context.locales[locale]?.name || locale %>
|
|
18
|
+
<% if (atRevisionLimit) { %><span class="text-sm text-raketa-foreground/50">(open revision limit reached)</span><% } %>
|
|
18
19
|
</label>
|
|
19
20
|
<% }) %>
|
|
20
21
|
</div>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
<div class="flex w-full pb-4">
|
|
7
7
|
<div class="flex items-center justify-between mb-4">
|
|
8
|
-
<div class="flex items-center border-b-2">
|
|
8
|
+
<div class="flex items-center border-b-2 border-neutral-200 dark:border-neutral-700">
|
|
9
9
|
<div class="<%= active === 'meta' ? activeClass : inactiveClass %>">
|
|
10
10
|
<a href="<%= `/${_context.locale}/settings/meta/edit` %>" class="text-raketa-primary font-semibold">
|
|
11
11
|
Meta and OpenGraph
|