@raketa-cloud/admin 1.9.4 → 1.9.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.
@@ -749,40 +749,6 @@ select {
749
749
  --tw-contain-style: ;
750
750
  }
751
751
 
752
- .container {
753
- width: 100%;
754
- }
755
-
756
- @media (min-width: 640px) {
757
- .container {
758
- max-width: 640px;
759
- }
760
- }
761
-
762
- @media (min-width: 768px) {
763
- .container {
764
- max-width: 768px;
765
- }
766
- }
767
-
768
- @media (min-width: 1024px) {
769
- .container {
770
- max-width: 1024px;
771
- }
772
- }
773
-
774
- @media (min-width: 1280px) {
775
- .container {
776
- max-width: 1280px;
777
- }
778
- }
779
-
780
- @media (min-width: 1536px) {
781
- .container {
782
- max-width: 1536px;
783
- }
784
- }
785
-
786
752
  /* Utils */
787
753
 
788
754
  .chip {
@@ -1862,9 +1828,9 @@ select {
1862
1828
  height: 3rem;
1863
1829
  }
1864
1830
 
1865
- .size-14 {
1866
- width: 3.5rem;
1867
- height: 3.5rem;
1831
+ .size-16 {
1832
+ width: 4rem;
1833
+ height: 4rem;
1868
1834
  }
1869
1835
 
1870
1836
  .size-3 {
@@ -1994,10 +1960,6 @@ select {
1994
1960
  width: 500px;
1995
1961
  }
1996
1962
 
1997
- .w-\[96px\] {
1998
- width: 96px;
1999
- }
2000
-
2001
1963
  .w-full {
2002
1964
  width: 100%;
2003
1965
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -37,7 +37,7 @@
37
37
  "chart.js": "4.5.1",
38
38
  "ejs": "6.0.1",
39
39
  "graphql-request": "7.4.0",
40
- "hono": "4.12.34",
40
+ "hono": "4.13.7",
41
41
  "lru-cache": "11.5.2"
42
42
  },
43
43
  "overrides": {
@@ -137,6 +137,17 @@ export default class RaketaClient {
137
137
  return { ...result };
138
138
  }
139
139
 
140
+ formatLocaleErrors(localeErrors) {
141
+ if (!localeErrors || !Array.isArray(localeErrors)) {
142
+ return [];
143
+ }
144
+
145
+ return localeErrors.map((item) => ({
146
+ locale: item.locale,
147
+ errors: this.formatErrors(item.errors || []),
148
+ }));
149
+ }
150
+
140
151
  buildFiltersInput(query) {
141
152
  if (!query) {
142
153
  return {};
@@ -7,11 +7,11 @@ import {
7
7
  CreateResourceDocument,
8
8
  DeleteResourceDocument,
9
9
  MiniResourcesDocument,
10
+ MultiTranslateResourceDocument,
10
11
  ResourceByIdDocument,
11
12
  ResourceChangePreviewDocument,
12
13
  ResourcesDocument,
13
14
  TranslateResourceDocument,
14
- buildBatchTranslateResourceDocument,
15
15
  } from "./documents/resourcesDocuments";
16
16
 
17
17
  const SETTINGS_DATE_DEFAULTS = {
@@ -156,7 +156,7 @@ export default class Resources extends RaketaClient {
156
156
  return { resource, errors };
157
157
  }
158
158
 
159
- appendWorkflow(input, workflow) {
159
+ appendWorkflow(input, workflow, previewParams = {}) {
160
160
  if (!workflow?.type) {
161
161
  return;
162
162
  }
@@ -165,9 +165,9 @@ export default class Resources extends RaketaClient {
165
165
  type: workflow.type,
166
166
  previewUrl: buildMagicPreviewAddress(
167
167
  {
168
- slug: input.resource.slug,
169
- locale: input.locale,
170
- resourceType: input.resource.resourceType,
168
+ slug: previewParams.slug,
169
+ locale: previewParams.locale,
170
+ resourceType: previewParams.resourceType,
171
171
  },
172
172
  this.getConfig(),
173
173
  ),
@@ -178,43 +178,85 @@ export default class Resources extends RaketaClient {
178
178
  }
179
179
  }
180
180
 
181
- async translateBatch(
181
+ async multiTranslate(
182
182
  sourceResourceId,
183
183
  locales,
184
184
  params,
185
185
  resourceType,
186
186
  workflow,
187
+ sourceLocale,
187
188
  ) {
188
189
  const paramsCopy = { ...params, resourceType };
189
190
  paramsCopy.widgets = JSON.stringify(paramsCopy.widgets);
190
191
  paramsCopy.settings = JSON.stringify(paramsCopy.settings);
191
192
 
192
- const BatchTranslateDocument = buildBatchTranslateResourceDocument(
193
- locales.length,
193
+ const translations = locales.map((locale) => ({
194
+ locale,
195
+ resource: paramsCopy,
196
+ }));
197
+
198
+ const input = {
199
+ sourceResourceId,
200
+ translations,
201
+ };
202
+
203
+ this.appendWorkflow(input, workflow, {
204
+ slug: paramsCopy.slug,
205
+ locale: sourceLocale,
206
+ resourceType,
207
+ });
208
+
209
+ const {
210
+ multiTranslateResource: { resources, errors, localeErrors },
211
+ } = await this.query(MultiTranslateResourceDocument, { input });
212
+
213
+ return { resources, errors, localeErrors };
214
+ }
215
+
216
+ async translateBatch(
217
+ sourceResourceId,
218
+ locales,
219
+ params,
220
+ resourceType,
221
+ workflow,
222
+ sourceLocale,
223
+ ) {
224
+ const { resources, errors, localeErrors } = await this.multiTranslate(
225
+ sourceResourceId,
226
+ locales,
227
+ params,
228
+ resourceType,
229
+ workflow,
230
+ sourceLocale,
194
231
  );
195
232
 
196
- const variables = {};
197
- locales.forEach((locale, i) => {
198
- const input = {
199
- locale,
200
- sourceResourceId,
201
- resource: paramsCopy,
202
- };
233
+ if (localeErrors && localeErrors.length > 0) {
234
+ const localeErrorMap = {};
235
+ localeErrors.forEach((le) => {
236
+ localeErrorMap[le.locale] = le.errors;
237
+ });
203
238
 
204
- this.appendWorkflow(input, workflow);
239
+ return locales.map((locale) => ({
240
+ resource: null,
241
+ errors: localeErrorMap[locale] || errors || null,
242
+ }));
243
+ }
205
244
 
206
- variables[`input${i}`] = input;
207
- });
245
+ if (errors) {
246
+ return locales.map(() => ({
247
+ resource: null,
248
+ errors,
249
+ }));
250
+ }
208
251
 
209
- const response = await this.query(BatchTranslateDocument, variables);
252
+ if (!resources) {
253
+ return null;
254
+ }
210
255
 
211
- return locales.map((_, i) => {
212
- const result = response[`translate${i}`];
213
- return {
214
- resource: result.resource,
215
- errors: result.errors,
216
- };
217
- });
256
+ return locales.map((_, i) => ({
257
+ resource: resources[i],
258
+ errors: null,
259
+ }));
218
260
  }
219
261
 
220
262
  async updateSlug(id, slug, locale, resourceType, redirect) {
@@ -15,7 +15,7 @@ export const ContentTasksDocument = gql`
15
15
  failedAt
16
16
  insertedAt
17
17
  updatedAt
18
- resource {
18
+ resources {
19
19
  id
20
20
  title
21
21
  slug
@@ -44,16 +44,7 @@ function resourceFragment() {
44
44
  tags ${tagFragment()}
45
45
  categories ${categoryFragment()}
46
46
  changes ${summaryChangeFragment()}
47
- activeContentTask {
48
- id
49
- workflow
50
- operator
51
- operatorMeta
52
- completedAt
53
- failedAt
54
- insertedAt
55
- updatedAt
56
- }
47
+ activeContentTask ${contentTaskFragment()}
57
48
  }
58
49
  `;
59
50
  }
@@ -195,22 +186,16 @@ const TranslateResourceDocument = gql`
195
186
  }
196
187
  `;
197
188
 
198
- const buildBatchTranslateResourceDocument = (count) => gql`
199
- mutation BatchTranslateResources(${Array(count)
200
- .fill(0)
201
- .map((_, i) => `$input${i}: TranslateResourceInput!`)
202
- .join(", ")}) {
203
- ${Array(count)
204
- .fill(0)
205
- .map(
206
- (_, i) => `
207
- translate${i}: translateResource(input: $input${i}) {
208
- resource ${resourceFragment()}
189
+ const MultiTranslateResourceDocument = gql`
190
+ mutation MultiTranslateResource($input: MultiTranslateResourceInput!) {
191
+ multiTranslateResource(input: $input) {
192
+ resources ${resourceFragment()}
193
+ errors ${errorsFragment()}
194
+ localeErrors {
195
+ locale
209
196
  errors ${errorsFragment()}
210
197
  }
211
- `,
212
- )
213
- .join("\n")}
198
+ }
214
199
  }
215
200
  `;
216
201
 
@@ -365,6 +350,7 @@ export {
365
350
  DeleteResourceDocument,
366
351
  ResourceAndSiteDocument,
367
352
  TranslateResourceDocument,
353
+ MultiTranslateResourceDocument,
368
354
  ResourceRevisionDocument,
369
355
  ResourceRevisionsDocument,
370
356
  StartResourceRevisionDocument,
@@ -375,7 +361,6 @@ export {
375
361
  ReopenResourceChangeDocument,
376
362
  LockResourceRevisionDocument,
377
363
  UnlockResourceRevisionDocument,
378
- buildBatchTranslateResourceDocument,
379
364
  ResourceHistoryDocument,
380
365
  buildBatchStartResourceRevisionDocument,
381
366
  ResourceEditorsDocument,
@@ -128,7 +128,7 @@ const create = async (ctx) => {
128
128
  : [ctx.resourceParams.resource.languages].filter(Boolean);
129
129
 
130
130
  const existingTranslations = Object.keys(blueprint.translations || {});
131
- const selectedLocales = formLanguages.filter(
131
+ const selectedLocales = [...new Set(formLanguages)].filter(
132
132
  (locale) =>
133
133
  locale !== originalLocale && !existingTranslations.includes(locale),
134
134
  );
@@ -153,15 +153,52 @@ const create = async (ctx) => {
153
153
 
154
154
  const workflow = ctx.resourceParams?.resource?.workflow;
155
155
 
156
- const translations = await ctx.client.resources.translateBatch(
157
- parent_id,
158
- selectedLocales,
159
- resource,
160
- resourceType,
161
- workflow,
162
- );
156
+ const { resources, errors, localeErrors } =
157
+ await ctx.client.resources.multiTranslate(
158
+ parent_id,
159
+ selectedLocales,
160
+ resource,
161
+ resourceType,
162
+ workflow,
163
+ originalLocale,
164
+ );
165
+
166
+ if (localeErrors && localeErrors.length > 0) {
167
+ return {
168
+ assigns: {
169
+ resource: {
170
+ ...resource,
171
+ translations: blueprint.translations || {},
172
+ languages: formLanguages,
173
+ workflow: workflow,
174
+ },
175
+ blueprint,
176
+ targetLocale: originalLocale,
177
+ errors: {
178
+ resource: errors ? ctx.client.resources.formatErrors(errors) : {},
179
+ localeErrors: ctx.client.resources.formatLocaleErrors(localeErrors),
180
+ },
181
+ },
182
+ };
183
+ }
184
+
185
+ if (errors) {
186
+ return {
187
+ assigns: {
188
+ resource: {
189
+ ...resource,
190
+ translations: blueprint.translations || {},
191
+ languages: formLanguages,
192
+ workflow: workflow,
193
+ },
194
+ blueprint,
195
+ targetLocale: originalLocale,
196
+ errors: { resource: ctx.client.resources.formatErrors(errors) },
197
+ },
198
+ };
199
+ }
163
200
 
164
- if (!translations) {
201
+ if (!resources) {
165
202
  return {
166
203
  assigns: {
167
204
  resource: {
@@ -184,8 +221,10 @@ const create = async (ctx) => {
184
221
  };
185
222
  }
186
223
 
187
- for (let i = 0; i < translations.length; i++) {
188
- const { resource: translated } = translations[i];
224
+ const revisionInputs = [];
225
+
226
+ for (let i = 0; i < resources.length; i++) {
227
+ const translated = resources[i];
189
228
  const locale = selectedLocales[i];
190
229
 
191
230
  if (translated) {
@@ -197,32 +236,7 @@ const create = async (ctx) => {
197
236
  visibility: translated.visibility,
198
237
  },
199
238
  };
200
- }
201
- }
202
-
203
- const revisionInputs = [];
204
-
205
- for (let i = 0; i < translations.length; i++) {
206
- const { errors, resource: translated } = translations[i];
207
- const locale = selectedLocales[i];
208
239
 
209
- if (errors) {
210
- return {
211
- assigns: {
212
- resource: {
213
- ...resource,
214
- translations: blueprint.translations || {},
215
- languages: formLanguages,
216
- workflow: workflow,
217
- },
218
- blueprint,
219
- targetLocale: locale,
220
- errors: { resource: ctx.client.resources.formatErrors(errors) },
221
- },
222
- };
223
- }
224
-
225
- if (translated) {
226
240
  translationIds.push(`${locale}:${translated.id}`);
227
241
  revisionInputs.push({
228
242
  resourceId: translated.id,
@@ -247,7 +261,7 @@ const create = async (ctx) => {
247
261
  languages: formLanguages,
248
262
  },
249
263
  blueprint,
250
- targetLocale: selectedLocales[i],
264
+ targetLocale: revisionInputs[i]?.locale || selectedLocales[i],
251
265
  errors: {
252
266
  resource: ctx.client.resources.formatErrors(revisionErrors),
253
267
  },
@@ -8,9 +8,10 @@ import {
8
8
  const ALGORITHM = "aes-256-gcm";
9
9
  const IV_LENGTH = 12;
10
10
  const TAG_LENGTH = 16;
11
+ const CHUNK_SIZE = 40;
11
12
 
12
13
  function getSecretKey(config = {}) {
13
- const secret = config.secrets.crypto_secret;
14
+ const secret = config.secrets?.crypto_secret;
14
15
 
15
16
  if (!secret) {
16
17
  throw new Error(
@@ -21,6 +22,40 @@ function getSecretKey(config = {}) {
21
22
  return createHash("sha256").update(secret).digest();
22
23
  }
23
24
 
25
+ export function chunkToken(token, chunkSize = CHUNK_SIZE) {
26
+ if (!token || typeof token !== "string") {
27
+ return token;
28
+ }
29
+
30
+ const chunks = [];
31
+ for (let i = 0; i < token.length; i += chunkSize) {
32
+ chunks.push(token.slice(i, i + chunkSize));
33
+ }
34
+
35
+ return chunks.join("-");
36
+ }
37
+
38
+ export function unchunkToken(token, chunkSize = CHUNK_SIZE) {
39
+ if (!token || typeof token !== "string") {
40
+ return token;
41
+ }
42
+
43
+ let result = "";
44
+ let i = 0;
45
+
46
+ while (i < token.length) {
47
+ const chunk = token.slice(i, i + chunkSize);
48
+ result += chunk;
49
+ i += chunkSize;
50
+
51
+ if (i < token.length && token[i] === "-") {
52
+ i += 1;
53
+ }
54
+ }
55
+
56
+ return result;
57
+ }
58
+
24
59
  export function generateMagicPreviewToken(
25
60
  { slug, resourceType, locale } = {},
26
61
  config = {},
@@ -40,17 +75,14 @@ export function generateMagicPreviewToken(
40
75
  ]);
41
76
  const tag = cipher.getAuthTag();
42
77
 
43
- return Buffer.concat([iv, tag, encrypted]).toString("base64url");
44
- }
78
+ const rawToken = Buffer.concat([iv, tag, encrypted]).toString("base64url");
45
79
 
46
- export function decipherMagicPreviewToken(token, config) {
47
- if (!token || typeof token !== "string") {
48
- return null;
49
- }
80
+ return chunkToken(rawToken);
81
+ }
50
82
 
83
+ function decipherPayload(rawToken, key) {
51
84
  try {
52
- const key = getSecretKey(config);
53
- const combined = Buffer.from(token, "base64url");
85
+ const combined = Buffer.from(rawToken, "base64url");
54
86
 
55
87
  if (combined.length < IV_LENGTH + TAG_LENGTH) {
56
88
  return null;
@@ -80,6 +112,31 @@ export function decipherMagicPreviewToken(token, config) {
80
112
  }
81
113
  }
82
114
 
115
+ export function decipherMagicPreviewToken(token, config) {
116
+ if (!token || typeof token !== "string") {
117
+ return null;
118
+ }
119
+
120
+ try {
121
+ const key = getSecretKey(config);
122
+
123
+ const unchunked = unchunkToken(token);
124
+ const payload = decipherPayload(unchunked, key);
125
+
126
+ if (payload) {
127
+ return payload;
128
+ }
129
+
130
+ if (unchunked !== token) {
131
+ return decipherPayload(token, key);
132
+ }
133
+
134
+ return null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
83
140
  export function buildMagicPreviewAddress(
84
141
  { slug, resourceType, locale },
85
142
  globalConfig,
@@ -232,6 +232,46 @@ describe("RaketaClient", () => {
232
232
  });
233
233
  });
234
234
 
235
+ describe("formatLocaleErrors", () => {
236
+ test("formats array of locale errors", () => {
237
+ const client = buildClient();
238
+ const localeErrors = [
239
+ {
240
+ locale: "fr",
241
+ errors: [{ key: "title", message: "Title is required" }],
242
+ },
243
+ {
244
+ locale: "de",
245
+ errors: [
246
+ { key: "slug", message: "Slug is taken" },
247
+ { key: "slug", message: "Slug is invalid" },
248
+ ],
249
+ },
250
+ ];
251
+
252
+ const result = client.formatLocaleErrors(localeErrors);
253
+
254
+ expect(result).toEqual([
255
+ {
256
+ locale: "fr",
257
+ errors: { title: "Title is required" },
258
+ },
259
+ {
260
+ locale: "de",
261
+ errors: { slug: "Slug is taken, Slug is invalid" },
262
+ },
263
+ ]);
264
+ });
265
+
266
+ test("returns empty array for invalid input", () => {
267
+ const client = buildClient();
268
+
269
+ expect(client.formatLocaleErrors(null)).toEqual([]);
270
+ expect(client.formatLocaleErrors(undefined)).toEqual([]);
271
+ expect(client.formatLocaleErrors({})).toEqual([]);
272
+ });
273
+ });
274
+
235
275
  describe("buildFiltersInput", () => {
236
276
  test("extracts filters with f: prefix", () => {
237
277
  const client = buildClient();
@@ -26,22 +26,26 @@ describe("ContentTasksHandler", () => {
26
26
  Factory.build("contentTask", {
27
27
  id: "1",
28
28
  workflow: "lokalise",
29
- resource: {
30
- id: "123",
31
- title: "Test Page",
32
- locale: "en",
33
- resourceType: "page",
34
- },
29
+ resources: [
30
+ {
31
+ id: "123",
32
+ title: "Test Page",
33
+ locale: "en",
34
+ resourceType: "page",
35
+ },
36
+ ],
35
37
  }),
36
38
  Factory.build("contentTask", {
37
39
  id: "2",
38
40
  workflow: "default",
39
- resource: {
40
- id: "456",
41
- title: "Test Movie",
42
- locale: "de",
43
- resourceType: "movie-type",
44
- },
41
+ resources: [
42
+ {
43
+ id: "456",
44
+ title: "Test Movie",
45
+ locale: "de",
46
+ resourceType: "movie-type",
47
+ },
48
+ ],
45
49
  }),
46
50
  ];
47
51