@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.10.4",
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/forms": "0.5.7",
27
- "@tailwindcss/typography": "0.5.13",
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": "29.7.0",
30
- "jest": "29.7.0",
31
- "jest-environment-node": "29.7.0",
32
- "tailwindcss": "3.4.4",
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": {
@@ -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,
@@ -28,6 +28,7 @@ export const ContentTasksDocument = gql`
28
28
  lokaliseWorkflow {
29
29
  id
30
30
  status
31
+ partial
31
32
  settings
32
33
  insertedAt
33
34
  updatedAt
@@ -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
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
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 body-2 p-4 rounded sticky top-6 dark:bg-raketa-primary/20;
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 body-1 w-full text-raketa-foreground/60 rounded-t overflow-hidden;
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 {