@raketa-cloud/admin 1.10.4 → 1.10.6
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/dist/application.css +2664 -3533
- package/package.json +8 -7
- package/src/cloud/Resources.js +39 -0
- package/src/cloud/documents/contentTasksDocuments.js +1 -0
- package/src/cloud/documents/resourcesDocuments.js +12 -0
- package/src/handlers/resources/PartialTranslationsHandler.js +279 -0
- package/src/routeResourceModule.js +11 -0
- package/src/styles/application.css +27 -5
- package/src/tests/handlers/resources/PartialTranslationsHandler.test.js +279 -0
- package/src/tests/routeResourceModule.test.js +26 -0
- package/src/tests/views/due_date.test.js +121 -0
- package/src/tests/views/partial_translations.test.js +50 -0
- package/src/views/_shared/_content_tasks.ejs +46 -20
- package/src/views/resources/_shared/_due_date.ejs +71 -0
- package/src/views/resources/partial-translations/create_form.ejs +72 -0
- package/src/views/resources/translations/create_form.ejs +4 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raketa-cloud/admin",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"@babel/preset-env": "7.29.0",
|
|
24
24
|
"@babel/preset-typescript": "7.28.5",
|
|
25
25
|
"@repo/typescript-config": "*",
|
|
26
|
-
"@tailwindcss/
|
|
27
|
-
"@tailwindcss/
|
|
26
|
+
"@tailwindcss/cli": "4.3.3",
|
|
27
|
+
"@tailwindcss/forms": "0.5.11",
|
|
28
|
+
"@tailwindcss/typography": "0.5.20",
|
|
28
29
|
"@types/node": "24.10.1",
|
|
29
|
-
"babel-jest": "
|
|
30
|
-
"jest": "
|
|
31
|
-
"jest-environment-node": "
|
|
32
|
-
"tailwindcss": "3.
|
|
30
|
+
"babel-jest": "30.5.2",
|
|
31
|
+
"jest": "30.5.2",
|
|
32
|
+
"jest-environment-node": "30.5.2",
|
|
33
|
+
"tailwindcss": "4.3.3",
|
|
33
34
|
"typescript": "5.9.3"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
package/src/cloud/Resources.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CreateResourceDocument,
|
|
8
8
|
DeleteResourceDocument,
|
|
9
9
|
MiniResourcesDocument,
|
|
10
|
+
MultiPartialTranslateResourceDocument,
|
|
10
11
|
MultiTranslateResourceDocument,
|
|
11
12
|
ResourceByIdDocument,
|
|
12
13
|
ResourceChangePreviewDocument,
|
|
@@ -38,6 +39,17 @@ export default class Resources extends RaketaClient {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
previewUrl(resource, locale) {
|
|
43
|
+
return buildMagicPreviewAddress(
|
|
44
|
+
{
|
|
45
|
+
slug: resource.slug,
|
|
46
|
+
locale,
|
|
47
|
+
resourceType: resource.resourceType,
|
|
48
|
+
},
|
|
49
|
+
this.getConfig(),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
visibilityOptions() {
|
|
42
54
|
return [
|
|
43
55
|
["private", "Private"],
|
|
@@ -213,6 +225,33 @@ export default class Resources extends RaketaClient {
|
|
|
213
225
|
return { resources, errors, localeErrors };
|
|
214
226
|
}
|
|
215
227
|
|
|
228
|
+
async multiPartialTranslate(
|
|
229
|
+
sourceResourceId,
|
|
230
|
+
locales,
|
|
231
|
+
widgets,
|
|
232
|
+
workflow,
|
|
233
|
+
resource,
|
|
234
|
+
) {
|
|
235
|
+
const input = {
|
|
236
|
+
sourceResourceId,
|
|
237
|
+
translations: locales.map((locale) => ({
|
|
238
|
+
locale,
|
|
239
|
+
widgets: JSON.stringify(widgets),
|
|
240
|
+
})),
|
|
241
|
+
};
|
|
242
|
+
this.appendWorkflow(input, workflow, {
|
|
243
|
+
slug: resource.slug,
|
|
244
|
+
locale: resource.locale,
|
|
245
|
+
resourceType: resource.resourceType,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const {
|
|
249
|
+
multiPartialTranslateResource: { contentTask, errors },
|
|
250
|
+
} = await this.query(MultiPartialTranslateResourceDocument, { input });
|
|
251
|
+
|
|
252
|
+
return { contentTask, errors };
|
|
253
|
+
}
|
|
254
|
+
|
|
216
255
|
async translateBatch(
|
|
217
256
|
sourceResourceId,
|
|
218
257
|
locales,
|
|
@@ -199,6 +199,17 @@ const MultiTranslateResourceDocument = gql`
|
|
|
199
199
|
}
|
|
200
200
|
`;
|
|
201
201
|
|
|
202
|
+
const MultiPartialTranslateResourceDocument = gql`
|
|
203
|
+
mutation MultiPartialTranslateResource(
|
|
204
|
+
$input: MultiPartialTranslateResourceInput!
|
|
205
|
+
) {
|
|
206
|
+
multiPartialTranslateResource(input: $input) {
|
|
207
|
+
contentTask ${contentTaskFragment()}
|
|
208
|
+
errors ${errorsFragment()}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
|
|
202
213
|
const ResourceRevisionsDocument = gql`
|
|
203
214
|
query ResourceRevisions($input: IdTypeInput!) {
|
|
204
215
|
resourceRevisions(input: $input) {
|
|
@@ -351,6 +362,7 @@ export {
|
|
|
351
362
|
ResourceAndSiteDocument,
|
|
352
363
|
TranslateResourceDocument,
|
|
353
364
|
MultiTranslateResourceDocument,
|
|
365
|
+
MultiPartialTranslateResourceDocument,
|
|
354
366
|
ResourceRevisionDocument,
|
|
355
367
|
ResourceRevisionsDocument,
|
|
356
368
|
StartResourceRevisionDocument,
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const toArray = (value) =>
|
|
2
|
+
Array.isArray(value) ? value : [value].filter(Boolean);
|
|
3
|
+
|
|
4
|
+
const isActiveContentTask = (task) =>
|
|
5
|
+
task && !task.completedAt && !task.failedAt;
|
|
6
|
+
|
|
7
|
+
const hasActiveContentTask = (resource) =>
|
|
8
|
+
isActiveContentTask(resource.activeContentTask) ||
|
|
9
|
+
Object.values(resource.translations || {}).some((translation) =>
|
|
10
|
+
isActiveContentTask(translation.active_content_task),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const hasTranslations = (resource) =>
|
|
14
|
+
Object.values(resource.translations || {}).some(
|
|
15
|
+
(translation) => translation?.id,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const targetTranslationLocales = (resource, sourceLocale) =>
|
|
19
|
+
Object.entries(resource.translations || {})
|
|
20
|
+
.filter(
|
|
21
|
+
([locale, translation]) => locale !== sourceLocale && translation?.id,
|
|
22
|
+
)
|
|
23
|
+
.map(([locale]) => locale);
|
|
24
|
+
|
|
25
|
+
const selectedWidgetIds = (resource, query = {}) => {
|
|
26
|
+
const requested = Array.isArray(query.widget_ids)
|
|
27
|
+
? query.widget_ids
|
|
28
|
+
: String(query.widget_ids || "")
|
|
29
|
+
.split(",")
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
|
|
32
|
+
return [...new Set(requested)];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const partitionWidgetIds = (resource, widgetIds) => {
|
|
36
|
+
const resourceWidgetIds = new Set(
|
|
37
|
+
(resource.widgets || []).map((widget) => widget.widgetId).filter(Boolean),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
validWidgetIds: widgetIds.filter((widgetId) =>
|
|
42
|
+
resourceWidgetIds.has(widgetId),
|
|
43
|
+
),
|
|
44
|
+
unknownWidgetIds: widgetIds.filter(
|
|
45
|
+
(widgetId) => !resourceWidgetIds.has(widgetId),
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formAssigns = (resource, query, previewUrl, values = {}, errors = {}) => {
|
|
51
|
+
const submittedWidgetIds = values.widgetIds ?? values.widget_ids;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
resource,
|
|
55
|
+
previewUrl,
|
|
56
|
+
partialTranslation: {
|
|
57
|
+
locales: [],
|
|
58
|
+
workflow: {},
|
|
59
|
+
...values,
|
|
60
|
+
widgetIds:
|
|
61
|
+
submittedWidgetIds === undefined
|
|
62
|
+
? selectedWidgetIds(resource, query)
|
|
63
|
+
: toArray(submittedWidgetIds),
|
|
64
|
+
},
|
|
65
|
+
errors,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const validatePartialTranslation = ({
|
|
70
|
+
resource,
|
|
71
|
+
locales,
|
|
72
|
+
widgetIds,
|
|
73
|
+
workflow,
|
|
74
|
+
unknownWidgetIds,
|
|
75
|
+
unknownLocales,
|
|
76
|
+
}) => {
|
|
77
|
+
const errors = [];
|
|
78
|
+
|
|
79
|
+
if (locales.length === 0) {
|
|
80
|
+
errors.push({
|
|
81
|
+
key: "locales",
|
|
82
|
+
message: "select at least one existing translation",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (widgetIds.length === 0) {
|
|
86
|
+
errors.push({ key: "widgetIds", message: "can't be blank" });
|
|
87
|
+
}
|
|
88
|
+
if (unknownLocales.length > 0) {
|
|
89
|
+
errors.push({
|
|
90
|
+
key: "locales",
|
|
91
|
+
message: `not found in the resource translations: ${unknownLocales.join(", ")}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!workflow.type) {
|
|
95
|
+
errors.push({ key: "workflow", message: "can't be blank" });
|
|
96
|
+
}
|
|
97
|
+
if (unknownWidgetIds.length > 0) {
|
|
98
|
+
errors.push({
|
|
99
|
+
key: "widgetIds",
|
|
100
|
+
message: `not found in the source resource: ${unknownWidgetIds.join(", ")}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!hasTranslations(resource)) {
|
|
104
|
+
errors.push({
|
|
105
|
+
key: "generalError",
|
|
106
|
+
message:
|
|
107
|
+
"cannot start a partial translation without existing translations",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (hasActiveContentTask(resource)) {
|
|
111
|
+
errors.push({
|
|
112
|
+
key: "generalError",
|
|
113
|
+
message:
|
|
114
|
+
"cannot start a partial translation while a translation task is in progress",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return errors;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const create_form = async (ctx) => {
|
|
122
|
+
const { parent_id } = ctx.params;
|
|
123
|
+
const { resourceType } = ctx.resourceConfig;
|
|
124
|
+
const resource = await ctx.client.resources.find(
|
|
125
|
+
parent_id,
|
|
126
|
+
ctx.locale,
|
|
127
|
+
resourceType,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!resource) {
|
|
131
|
+
throw new Error(`Cannot find ${resourceType} with id ${parent_id}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const widgetIds = selectedWidgetIds(resource, ctx.query);
|
|
135
|
+
const { validWidgetIds, unknownWidgetIds } = partitionWidgetIds(
|
|
136
|
+
resource,
|
|
137
|
+
widgetIds,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const previewUrl = ctx.client.resources.previewUrl(
|
|
141
|
+
{ ...resource, resourceType },
|
|
142
|
+
ctx.locale,
|
|
143
|
+
);
|
|
144
|
+
const validationErrors = [];
|
|
145
|
+
|
|
146
|
+
if (widgetIds.length === 0) {
|
|
147
|
+
validationErrors.push({ key: "widgetIds", message: "can't be blank" });
|
|
148
|
+
}
|
|
149
|
+
if (unknownWidgetIds.length > 0) {
|
|
150
|
+
validationErrors.push({
|
|
151
|
+
key: "widgetIds",
|
|
152
|
+
message: `not found in the source resource: ${unknownWidgetIds.join(", ")}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!hasTranslations(resource)) {
|
|
156
|
+
validationErrors.push({
|
|
157
|
+
key: "generalError",
|
|
158
|
+
message:
|
|
159
|
+
"cannot start a partial translation without existing translations",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (hasActiveContentTask(resource)) {
|
|
163
|
+
validationErrors.push({
|
|
164
|
+
key: "generalError",
|
|
165
|
+
message:
|
|
166
|
+
"cannot start a partial translation while a translation task is in progress",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (validationErrors.length > 0) {
|
|
171
|
+
return {
|
|
172
|
+
assigns: formAssigns(
|
|
173
|
+
resource,
|
|
174
|
+
ctx.query,
|
|
175
|
+
previewUrl,
|
|
176
|
+
{ widgetIds: validWidgetIds },
|
|
177
|
+
{
|
|
178
|
+
partialTranslation:
|
|
179
|
+
ctx.client.resources.formatErrors(validationErrors),
|
|
180
|
+
},
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { assigns: formAssigns(resource, ctx.query, previewUrl) };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const create = async (ctx) => {
|
|
189
|
+
const { parent_id } = ctx.params;
|
|
190
|
+
const { resourceType, resourceTypePlural } = ctx.resourceConfig;
|
|
191
|
+
const resource = await ctx.client.resources.find(
|
|
192
|
+
parent_id,
|
|
193
|
+
ctx.locale,
|
|
194
|
+
resourceType,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!resource) {
|
|
198
|
+
throw new Error(`Cannot find ${resourceType} with id ${parent_id}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const previewUrl = ctx.client.resources.previewUrl(
|
|
202
|
+
{ ...resource, resourceType },
|
|
203
|
+
ctx.locale,
|
|
204
|
+
);
|
|
205
|
+
const values = ctx.resourceParams?.partial_translation || {};
|
|
206
|
+
const locales = toArray(values.locales);
|
|
207
|
+
const widgetIds =
|
|
208
|
+
values.widget_ids === undefined
|
|
209
|
+
? selectedWidgetIds({ widgets: [] }, ctx.query)
|
|
210
|
+
: toArray(values.widget_ids);
|
|
211
|
+
const workflow = values.workflow || {};
|
|
212
|
+
const { validWidgetIds, unknownWidgetIds } = partitionWidgetIds(
|
|
213
|
+
resource,
|
|
214
|
+
widgetIds,
|
|
215
|
+
);
|
|
216
|
+
const validLocales = targetTranslationLocales(resource, ctx.locale);
|
|
217
|
+
const unknownLocales = locales.filter(
|
|
218
|
+
(locale) => !validLocales.includes(locale),
|
|
219
|
+
);
|
|
220
|
+
const selectedValidLocales = locales.filter((locale) =>
|
|
221
|
+
validLocales.includes(locale),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const validationErrors = validatePartialTranslation({
|
|
225
|
+
resource,
|
|
226
|
+
locales,
|
|
227
|
+
widgetIds,
|
|
228
|
+
workflow,
|
|
229
|
+
unknownWidgetIds,
|
|
230
|
+
unknownLocales,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (validationErrors.length > 0) {
|
|
234
|
+
return {
|
|
235
|
+
assigns: formAssigns(
|
|
236
|
+
resource,
|
|
237
|
+
ctx.query,
|
|
238
|
+
previewUrl,
|
|
239
|
+
{
|
|
240
|
+
...values,
|
|
241
|
+
locales: selectedValidLocales,
|
|
242
|
+
widgetIds: validWidgetIds,
|
|
243
|
+
workflow,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
partialTranslation:
|
|
247
|
+
ctx.client.resources.formatErrors(validationErrors),
|
|
248
|
+
},
|
|
249
|
+
),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const widgets = (resource.widgets || []).filter((widget) =>
|
|
254
|
+
validWidgetIds.includes(widget.widgetId),
|
|
255
|
+
);
|
|
256
|
+
const { errors } = await ctx.client.resources.multiPartialTranslate(
|
|
257
|
+
parent_id,
|
|
258
|
+
locales,
|
|
259
|
+
widgets,
|
|
260
|
+
workflow,
|
|
261
|
+
{ ...resource, locale: ctx.locale, resourceType },
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (errors) {
|
|
265
|
+
return {
|
|
266
|
+
assigns: formAssigns(
|
|
267
|
+
resource,
|
|
268
|
+
ctx.query,
|
|
269
|
+
previewUrl,
|
|
270
|
+
{ ...values, locales, widgetIds: validWidgetIds, workflow },
|
|
271
|
+
{ partialTranslation: ctx.client.resources.formatErrors(errors) },
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { redirect: `/${ctx.locale}/${resourceTypePlural}/${parent_id}` };
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export default { create_form, create };
|
|
@@ -15,6 +15,7 @@ import ClonesHandler from "./handlers/resources/ClonesHandler";
|
|
|
15
15
|
import DeletesHandler from "./handlers/resources/DeletesHandler";
|
|
16
16
|
import ExportsHandler from "./handlers/resources/ExportsHandler";
|
|
17
17
|
import HistoryHandler from "./handlers/resources/HistoryHandler";
|
|
18
|
+
import PartialTranslationsHandler from "./handlers/resources/PartialTranslationsHandler";
|
|
18
19
|
import ReportsAPIHandler from "./handlers/resources/ReportsAPIHandler";
|
|
19
20
|
import ResourcesAPIHandler from "./handlers/resources/ResourcesAPIHandler";
|
|
20
21
|
import ResourcesHandler from "./handlers/resources/ResourcesHandler";
|
|
@@ -93,6 +94,16 @@ function routeResourceModule(
|
|
|
93
94
|
{ ...resourceConfig, viewsFallback: "resources/translations" },
|
|
94
95
|
);
|
|
95
96
|
|
|
97
|
+
if (adapter.globalConfig?.features?.contentTasks) {
|
|
98
|
+
routeNestedCollection(
|
|
99
|
+
adapter,
|
|
100
|
+
baseResource,
|
|
101
|
+
"partial-translations",
|
|
102
|
+
PartialTranslationsHandler,
|
|
103
|
+
{ ...resourceConfig, viewsFallback: "resources/partial-translations" },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
routeNestedCollection(adapter, baseResource, "history", HistoryHandler, {
|
|
97
108
|
...resourceConfig,
|
|
98
109
|
viewsFallback: "resources/history",
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
@
|
|
2
|
-
@
|
|
3
|
-
@
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "@tailwindcss/forms";
|
|
3
|
+
@plugin "@tailwindcss/typography";
|
|
4
|
+
@source "../**/*.{html,js,ejs}";
|
|
5
|
+
@source "../../../apps/**/*.{html,js,ejs}";
|
|
6
|
+
|
|
7
|
+
@theme {
|
|
8
|
+
--color-raketa-background: rgb(var(--background));
|
|
9
|
+
--color-raketa-foreground: rgb(var(--foreground));
|
|
10
|
+
--color-raketa-primary: rgb(var(--primary));
|
|
11
|
+
--color-raketa-primary-light: rgb(var(--primary-light));
|
|
12
|
+
--color-raketa-primary-dark: rgb(var(--primary-dark));
|
|
13
|
+
--color-mono-purple-25: #f0f0f8;
|
|
14
|
+
--color-mono-purple-100: #9d9dd2;
|
|
15
|
+
--color-mono-purple-300: #6e6eb2;
|
|
16
|
+
--color-mono-25: #eaeaea;
|
|
17
|
+
--color-mono-50: #d0d1d2;
|
|
18
|
+
--color-mono-100: #f6f6f6;
|
|
19
|
+
--color-mono-200: #b7b7b7;
|
|
20
|
+
--color-mono-300: #737373;
|
|
21
|
+
--color-mono-800: #33333c;
|
|
22
|
+
--color-mono-850: #232324;
|
|
23
|
+
--color-mono-900: #1a1a1a;
|
|
24
|
+
--color-mono-950: #1c1c1e;
|
|
25
|
+
}
|
|
4
26
|
|
|
5
27
|
:root {
|
|
6
28
|
--background: 255, 255, 255;
|
|
@@ -40,7 +62,7 @@ body {
|
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
.help {
|
|
43
|
-
@apply bg-orange-50
|
|
65
|
+
@apply bg-orange-50 leading-[1.5] text-sm p-4 rounded sticky top-6 dark:bg-raketa-primary/20;
|
|
44
66
|
|
|
45
67
|
.heading-3 {
|
|
46
68
|
@apply text-raketa-foreground;
|
|
@@ -228,7 +250,7 @@ body {
|
|
|
228
250
|
|
|
229
251
|
/* Tables */
|
|
230
252
|
.data-grid {
|
|
231
|
-
@apply text-sm text-left
|
|
253
|
+
@apply text-sm text-left leading-[1.5] text-base w-full text-raketa-foreground/60 rounded-t overflow-hidden;
|
|
232
254
|
|
|
233
255
|
th,
|
|
234
256
|
td {
|