@raketa-cloud/admin 1.8.15 → 1.8.17

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.8.15",
3
+ "version": "1.8.17",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -13,6 +13,7 @@ import history from "./helpers/historyUtils";
13
13
  import pagination from "./helpers/pagination_helper";
14
14
  import sort_button from "./helpers/sort_button";
15
15
  import utils from "./helpers/utils";
16
+ import * as content_tasks from "./helpers/content_tasks_helper";
16
17
 
17
18
  function escapeHtml(string) {
18
19
  const escape = {
@@ -168,6 +169,7 @@ export default class HonoAdapter {
168
169
  sort_button,
169
170
  pagination,
170
171
  Inflector,
172
+ content_tasks,
171
173
  };
172
174
  }
173
175
 
@@ -1,28 +1,11 @@
1
- function buildResourceUrl(resource, resourceRoutes) {
2
- if (!resource) return "#";
3
-
4
- const { locale, resourceType, id } = resource;
5
-
6
- const route = resourceRoutes[resourceType];
7
-
8
- return `/${locale}/${route}/${id}`;
9
- }
10
-
11
1
  async function index(ctx) {
12
2
  const items = await ctx.client.contentTasks.list(ctx.query);
13
3
  const editors = await ctx.client.contentTasks.editors();
14
- const resourceRoutes = ctx.globalConfig.resourceRoutes;
15
-
16
- if (!resourceRoutes) {
17
- throw new Error("Missing resourceRoutes in the admin config!");
18
- }
19
4
 
20
5
  return {
21
6
  assigns: {
22
7
  items,
23
8
  editors: editors.list || [],
24
- buildResourceUrl: (resource) =>
25
- buildResourceUrl(resource, resourceRoutes),
26
9
  },
27
10
  };
28
11
  }
@@ -252,14 +252,10 @@ const create = async (ctx) => {
252
252
  };
253
253
  }
254
254
  }
255
-
256
- return {
257
- redirect: `/${originalLocale}/${resourceTypePlural}/${parent_id}/translations?translations=${translationIds.join(";")}`,
258
- };
259
255
  }
260
256
 
261
257
  return {
262
- redirect: `/${originalLocale}/${resourceTypePlural}/${parent_id}`,
258
+ redirect: `/${originalLocale}/${resourceTypePlural}/${parent_id}/translations?translations=${translationIds.join(";")}`,
263
259
  };
264
260
  };
265
261
 
@@ -0,0 +1,25 @@
1
+ const WORKFLOWS_MAP = {
2
+ lokalise_to_ai_rt_hum: "Lokalise: AI translation & Human review",
3
+ lokalise_tt_hum: "Lokalise: Human translation",
4
+ lokalise_tt_ai: "Lokalise: AI translation",
5
+ };
6
+
7
+ export const workflowsMap = (workflowType) => {
8
+ return WORKFLOWS_MAP[workflowType] || "Unknown workflow";
9
+ };
10
+
11
+ export const buildResourceUrl = (resource, globalConfig) => {
12
+ if (!resource) return "#";
13
+
14
+ const resourceRoutes = globalConfig.resourceRoutes;
15
+
16
+ if (!resourceRoutes) {
17
+ throw new Error("Missing resourceRoutes in the admin config!");
18
+ }
19
+
20
+ const { locale, resourceType, id } = resource;
21
+
22
+ const route = resourceRoutes[resourceType];
23
+
24
+ return `/${locale}/${route}/${id}`;
25
+ };
@@ -21,7 +21,7 @@ describe("ContentTasksHandler", () => {
21
21
  };
22
22
  }
23
23
 
24
- test("index - returns content tasks with editors and buildResourceUrl helper", async () => {
24
+ test("index - returns content tasks with editors", async () => {
25
25
  const contentTasks = [
26
26
  Factory.build("contentTask", {
27
27
  id: "1",
@@ -76,86 +76,5 @@ describe("ContentTasksHandler", () => {
76
76
  pagination: { currentPage: 1, pagesCount: 1 },
77
77
  });
78
78
  expect(result.assigns.editors).toEqual(editors);
79
- expect(typeof result.assigns.buildResourceUrl).toBe("function");
80
- });
81
-
82
- test("index - throws error when resourceRoutes is missing", async () => {
83
- const ctx = buildCtx();
84
- ctx.globalConfig = {}; // No resourceRoutes
85
-
86
- const mockedList = jest
87
- .spyOn(ctx.client.contentTasks, "list")
88
- .mockImplementation(() => ({ list: [], pagination: {} }));
89
-
90
- const mockedEditors = jest
91
- .spyOn(ctx.client.contentTasks, "editors")
92
- .mockImplementation(() => ({ list: [] }));
93
-
94
- await expect(ContentTasksHandler.index(ctx)).rejects.toThrow(
95
- "Missing resourceRoutes in the admin config!",
96
- );
97
- });
98
-
99
- test("buildResourceUrl helper - builds correct URL for standard resource", async () => {
100
- const ctx = buildCtx();
101
-
102
- const mockedList = jest
103
- .spyOn(ctx.client.contentTasks, "list")
104
- .mockImplementation(() => ({ list: [], pagination: {} }));
105
-
106
- const mockedEditors = jest
107
- .spyOn(ctx.client.contentTasks, "editors")
108
- .mockImplementation(() => ({ list: [] }));
109
-
110
- const result = await ContentTasksHandler.index(ctx);
111
- const { buildResourceUrl } = result.assigns;
112
-
113
- const resource = {
114
- id: "123",
115
- locale: "en",
116
- resourceType: "page",
117
- };
118
-
119
- expect(buildResourceUrl(resource)).toBe("/en/pages/123");
120
- });
121
-
122
- test("buildResourceUrl helper - builds correct URL for custom route", async () => {
123
- const ctx = buildCtx();
124
-
125
- const mockedList = jest
126
- .spyOn(ctx.client.contentTasks, "list")
127
- .mockImplementation(() => ({ list: [], pagination: {} }));
128
-
129
- const mockedEditors = jest
130
- .spyOn(ctx.client.contentTasks, "editors")
131
- .mockImplementation(() => ({ list: [] }));
132
-
133
- const result = await ContentTasksHandler.index(ctx);
134
- const { buildResourceUrl } = result.assigns;
135
-
136
- const resource = {
137
- id: "456",
138
- locale: "de",
139
- resourceType: "movie-type",
140
- };
141
-
142
- expect(buildResourceUrl(resource)).toBe("/de/movie-types/456");
143
- });
144
-
145
- test("buildResourceUrl helper - returns # for null resource", async () => {
146
- const ctx = buildCtx();
147
-
148
- const mockedList = jest
149
- .spyOn(ctx.client.contentTasks, "list")
150
- .mockImplementation(() => ({ list: [], pagination: {} }));
151
-
152
- const mockedEditors = jest
153
- .spyOn(ctx.client.contentTasks, "editors")
154
- .mockImplementation(() => ({ list: [] }));
155
-
156
- const result = await ContentTasksHandler.index(ctx);
157
- const { buildResourceUrl } = result.assigns;
158
-
159
- expect(buildResourceUrl(null)).toBe("#");
160
79
  });
161
80
  });
@@ -0,0 +1,68 @@
1
+ import {
2
+ workflowsMap,
3
+ buildResourceUrl,
4
+ } from "../../helpers/content_tasks_helper";
5
+
6
+ describe("content_tasks_helper", () => {
7
+ describe("workflowsMap", () => {
8
+ test("returns correct workflow name for known type", () => {
9
+ expect(workflowsMap("lokalise_to_ai_rt_hum")).toBe(
10
+ "Lokalise: AI translation & Human review",
11
+ );
12
+ expect(workflowsMap("lokalise_tt_hum")).toBe(
13
+ "Lokalise: Human translation",
14
+ );
15
+ expect(workflowsMap("lokalise_tt_ai")).toBe("Lokalise: AI translation");
16
+ });
17
+
18
+ test("returns 'Unknown workflow' for unknown type", () => {
19
+ expect(workflowsMap("unknown")).toBe("Unknown workflow");
20
+ });
21
+ });
22
+
23
+ describe("buildResourceUrl", () => {
24
+ const globalConfig = {
25
+ resourceRoutes: {
26
+ page: "pages",
27
+ author: "authors",
28
+ article: "articles",
29
+ "movie-type": "movie-types",
30
+ },
31
+ };
32
+
33
+ test("builds correct URL for standard resource", () => {
34
+ const resource = {
35
+ id: "123",
36
+ locale: "en",
37
+ resourceType: "page",
38
+ };
39
+ expect(buildResourceUrl(resource, globalConfig)).toBe("/en/pages/123");
40
+ });
41
+
42
+ test("builds correct URL for custom route", () => {
43
+ const resource = {
44
+ id: "456",
45
+ locale: "de",
46
+ resourceType: "movie-type",
47
+ };
48
+ expect(buildResourceUrl(resource, globalConfig)).toBe(
49
+ "/de/movie-types/456",
50
+ );
51
+ });
52
+
53
+ test("returns # for null resource", () => {
54
+ expect(buildResourceUrl(null, globalConfig)).toBe("#");
55
+ });
56
+
57
+ test("throws error when resourceRoutes is missing", () => {
58
+ const resource = {
59
+ id: "123",
60
+ locale: "en",
61
+ resourceType: "page",
62
+ };
63
+ expect(() => buildResourceUrl(resource, {})).toThrow(
64
+ "Missing resourceRoutes in the admin config!",
65
+ );
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,110 @@
1
+ <% if (tasks.length > 0) { %>
2
+ <table class="data-grid alt">
3
+ <thead>
4
+ <tr>
5
+ <% if (showAllDetails) { %>
6
+ <th>Resource</th>
7
+ <th>Locale</th>
8
+ <% } %>
9
+ <th>Workflow</th>
10
+ <th>Started at</th>
11
+ <% if (showAllDetails) { %>
12
+ <th>Finished at</th>
13
+ <% } %>
14
+ <th>Status</th>
15
+ </tr>
16
+ </thead>
17
+
18
+ <tbody>
19
+ <% tasks.forEach(task => { %>
20
+ <tr>
21
+ <% if (showAllDetails) { %>
22
+ <td class="w-1/3">
23
+ <% if (task.resource) { %>
24
+ <a href="<%= content_tasks.buildResourceUrl(task.resource, _context.globalConfig) %>" target="_blank" class="btn-text">
25
+ <%= task.resource.title %>
26
+ </a>
27
+
28
+ <div class="body-3 text-raketa-foreground/50 mt-1">
29
+ <strong>Type:</strong> <%= Inflector.humanize(Inflector.capitalize(task.resource.resourceType)) %>
30
+ </div>
31
+ <% } else { %>
32
+ <span class="text-raketa-foreground/50">—</span>
33
+ <% } %>
34
+ </td>
35
+
36
+ <td>
37
+ <% if (task.resource && _context.locales[task.resource.locale]) { %>
38
+ <% const { name, flag } = _context.locales[task.resource.locale] %>
39
+ <img title="<%= name %>" src="<%= flag %>" alt="<%= name %>" class="size-6 rounded-full border dark:border-neutral-700" />
40
+ <% } else { %>
41
+ <span class="text-raketa-foreground/50">—</span>
42
+ <% } %>
43
+ </td>
44
+ <% } %>
45
+
46
+ <td>
47
+ <span class="body-2">
48
+ <%= content_tasks.workflowsMap(task.workflow) %>
49
+ </span>
50
+ </td>
51
+
52
+ <td>
53
+ <div class="body-3 text-raketa-foreground/50">
54
+ <div class="flex items-center gap-1 mb-0.5">
55
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 text-raketa-foreground">
56
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
57
+ </svg>
58
+ <time data-pretty-datetime datetime="<%= task.insertedAt %>"><%= utils.formattedDateTime(task.insertedAt) %></time>
59
+ </div>
60
+
61
+ <div class="flex items-center gap-1" title="<%= task.operatorMeta?.name %>">
62
+ <% if (task.operatorMeta?.avatar) { %>
63
+ <img src="<%= task.operatorMeta?.avatar %>" class="size-4 rounded-full" alt="<%= task.operatorMeta?.name %>">
64
+ <% } else { %>
65
+ <div class="size-4 rounded-full"></div>
66
+ <% } %>
67
+ <%= task.operatorMeta?.name %>
68
+ </div>
69
+ </div>
70
+ </td>
71
+
72
+ <% if (showAllDetails) { %>
73
+ <td>
74
+ <% if (task.completedAt || task.failedAt) { %>
75
+ <div class="body-3 text-raketa-foreground/50">
76
+ <div class="flex items-center gap-1">
77
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 text-raketa-foreground">
78
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
79
+ </svg>
80
+ <time data-pretty-datetime datetime="<%= task.completedAt || task.failedAt %>">
81
+ <%= utils.formattedDateTime(task.completedAt || task.failedAt) %>
82
+ </time>
83
+ </div>
84
+ </div>
85
+ <% } else { %>
86
+ <span class="text-raketa-foreground/50">—</span>
87
+ <% } %>
88
+ </td>
89
+ <% } %>
90
+
91
+ <td>
92
+ <% if (task.completedAt) { %>
93
+ <span class="bg-emerald-100 chip text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300">
94
+ Completed
95
+ </span>
96
+ <% } else if (task.failedAt) { %>
97
+ <span class="bg-red-100 chip text-red-700 dark:bg-red-900/50 dark:text-red-300">
98
+ Failed
99
+ </span>
100
+ <% } else { %>
101
+ <span class="bg-blue-100 chip text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
102
+ In Progress
103
+ </span>
104
+ <% } %>
105
+ </td>
106
+ </tr>
107
+ <% }) %>
108
+ </tbody>
109
+ </table>
110
+ <% } %>
@@ -11,107 +11,7 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <table class="data-grid">
15
- <thead>
16
- <tr>
17
- <th>Resource</th>
18
- <th>Locale</th>
19
- <th>Workflow</th>
20
- <th>Started at</th>
21
- <th>Finished at</th>
22
- <th>Status</th>
23
- </tr>
24
- </thead>
25
-
26
- <tbody>
27
- <% if (items && items.list && items.list.length > 0) { %>
28
- <% items.list.forEach(task => { %>
29
- <tr>
30
- <td class="w-1/3">
31
- <% if (task.resource) { %>
32
- <a href="<%= buildResourceUrl(task.resource) %>" target="_blank" class="btn-text">
33
- <%= task.resource.title %>
34
- </a>
35
- <div class="body-3 text-raketa-foreground/50 mt-1">
36
- <strong>Type:</strong> <%= Inflector.humanize(Inflector.capitalize(task.resource.resourceType)) %>
37
- </div>
38
- <% } else { %>
39
- <span class="text-raketa-foreground/50">—</span>
40
- <% } %>
41
- </td>
42
-
43
- <td>
44
- <% if (task.resource && _context.locales[task.resource.locale]) { %>
45
- <% const { name, flag } = _context.locales[task.resource.locale] %>
46
- <img title="<%= name %>" src="<%= flag %>" alt="<%= name %>" class="size-6 rounded-full border dark:border-neutral-700" />
47
- <% } else { %>
48
- <span class="text-raketa-foreground/50">—</span>
49
- <% } %>
50
- </td>
51
-
52
- <td>
53
- <span class="body-2">
54
- <%= Inflector.capitalize(task.workflow) %>
55
- </span>
56
- </td>
57
-
58
- <td>
59
- <div class="body-3 text-raketa-foreground/50">
60
- <div class="flex items-center gap-1 mb-0.5">
61
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 text-raketa-foreground">
62
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
63
- </svg>
64
- <time data-pretty-datetime datetime="<%= task.insertedAt %>"><%= utils.formattedDateTime(task.insertedAt) %></time>
65
- </div>
66
-
67
- <div class="flex items-center gap-1" title="<%= task.operatorMeta?.name %>">
68
- <% if (task.operatorMeta?.avatar) { %>
69
- <img src="<%= task.operatorMeta?.avatar %>" class="size-4 rounded-full" alt="<%= task.operatorMeta?.name %>">
70
- <% } else { %>
71
- <div class="size-4 rounded-full"></div>
72
- <% } %>
73
- <%= task.operatorMeta?.name %>
74
- </div>
75
- </div>
76
- </td>
77
-
78
- <td>
79
- <% if (task.completedAt || task.failedAt) { %>
80
- <div class="body-3 text-raketa-foreground/50">
81
- <div class="flex items-center gap-1">
82
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 text-raketa-foreground">
83
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
84
- </svg>
85
- <time data-pretty-datetime datetime="<%= task.completedAt || task.failedAt %>">
86
- <%= utils.formattedDateTime(task.completedAt || task.failedAt) %>
87
- </time>
88
- </div>
89
- </div>
90
- <% } else { %>
91
- <span class="text-raketa-foreground/50">—</span>
92
- <% } %>
93
- </td>
94
-
95
- <td>
96
- <% if (task.completedAt) { %>
97
- <span class="bg-emerald-100 chip text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300">
98
- Completed
99
- </span>
100
- <% } else if (task.failedAt) { %>
101
- <span class="bg-red-100 chip text-red-700 dark:bg-red-900/50 dark:text-red-300">
102
- Failed
103
- </span>
104
- <% } else { %>
105
- <span class="bg-blue-100 chip text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
106
- In Progress
107
- </span>
108
- <% } %>
109
- </td>
110
- </tr>
111
- <% }) %>
112
- <% } %>
113
- </tbody>
114
- </table>
14
+ <%- render_partial('../_shared/_content_tasks.ejs', { tasks: items.list || [], showAllDetails: true }) %>
115
15
 
116
16
  <% if (!items || !items.list || items.list.length === 0) { %>
117
17
  <div class="empty-state">
@@ -166,6 +166,23 @@
166
166
  </div>
167
167
  <% } %>
168
168
 
169
+ <% if (utils.feature("contentTasks", _context) && resource.activeContentTask) { %>
170
+ <div class="mb-4">
171
+ <h4 class="flex items-center gap-2 pb-4 heading-3">
172
+ <svg class="size-4 text-raketa-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
173
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
174
+ </svg>
175
+
176
+ Active tasks
177
+ </h4>
178
+
179
+ <%- render_partial('../_shared/_content_tasks.ejs', {
180
+ tasks: [{...resource.activeContentTask, resource: resource}],
181
+ showAllDetails: false }
182
+ ) %>
183
+ </div>
184
+ <% } %>
185
+
169
186
  <%- render_partial('./_shared/_addons.ejs') %>
170
187
 
171
188
  <div class="mb-4">
@@ -82,7 +82,7 @@
82
82
  <div class="markdown">
83
83
  Select one or multiple locales to initiate bulk translations. The process depends on your selected workflow:
84
84
  <br />
85
- <ol>
85
+ <ul>
86
86
  <li>
87
87
  <strong>Default (Content Editors):</strong> A private copy and new revision are created immediately. The next screen provides quick links to translate settings and widgets so you can translate the content manually.
88
88
  </li>
@@ -95,10 +95,15 @@
95
95
  <li>
96
96
  <strong>Human Translation (Lokalise):</strong> A task is sent to Lokalise where a human translator fully translates and reviews the content.
97
97
  </li>
98
- </ol>
98
+ </ul>
99
99
 
100
100
  <br />
101
- <strong>Note:</strong> For all Lokalise workflows (<strong>AI, AI + Human</strong>, and <strong>Human</strong>), the private copy and new revision will be automatically created here in the CMS only after the external task is completed and returned.
101
+ <strong>Notes:</strong> For all Lokalise workflows (<strong>AI, AI + Human</strong>, and <strong>Human</strong>):
102
+ <ul>
103
+ <li>Content is required to start a Lokalise task.</li>
104
+ <li>The private copy and new revision will be automatically created here in the CMS only after the external task is completed and returned.</li>
105
+ <li>You can track translation progress in the <a href="<%= `/${_context.locale}/content_tasks` %>" target="_blank">Content tasks</a> module.</li>
106
+ </ul>
102
107
  </div>
103
108
  </div>
104
109
 
@@ -114,11 +119,11 @@
114
119
  </div>
115
120
 
116
121
  <div class="markdown">
117
- <ol>
122
+ <ul>
118
123
  <li>Select multiple locales to initiate bulk translations.</li>
119
124
  <li>A private copy of the <%= Inflector.downcase(_context.resourceConfig.resourceType) %> will be automatically created for each selected language, with a new revision opened to facilitate the translation process.</li>
120
125
  <li>On the next screen you'll see a list with quick links to transalte settings and widgets.</li>
121
- </ol>
126
+ </ul>
122
127
  </div>
123
128
  </div>
124
129
 
@@ -6,12 +6,18 @@
6
6
  </div>
7
7
  </div>
8
8
 
9
- <% if (translations && translations.length > 0) { %>
9
+ <% const activeContentTasks = translations?.some(t => Boolean(t.activeContentTask)) %>
10
+
11
+ <% if (translations.length > 0) { %>
10
12
  <table class="data-grid alt">
11
13
  <thead>
12
14
  <tr>
13
15
  <th>Locale</th>
14
- <th>&nbsp;</th>
16
+ <% if (activeContentTasks) { %>
17
+ <th>Status</th>
18
+ <% } else { %>
19
+ <th>&nbsp;</th>
20
+ <% } %>
15
21
  </tr>
16
22
  </thead>
17
23
  <tbody>
@@ -25,9 +31,25 @@
25
31
  </div>
26
32
  </td>
27
33
 
28
- <td class="whitespace-nowrap">
29
- <%- render_partial('./_translation_toolbar.ejs', { translation }) %>
30
- </td>
34
+ <% if (activeContentTasks) { %>
35
+ <td class="whitespace-nowrap">
36
+ <a href="<%= `/${_context.locale}/content_tasks`%>" target="blank">
37
+ <%= content_tasks.workflowsMap(translation.activeContentTask.workflow) %>
38
+
39
+ <% if (translation.activeContentTask.completedAt) { %>
40
+ completed
41
+ <% } else if (translation.activeContentTask.failedAt) { %>
42
+ failed
43
+ <% } else { %>
44
+ in Progress
45
+ <% } %>
46
+ </a>
47
+ </td>
48
+ <% } else { %>
49
+ <td class="whitespace-nowrap">
50
+ <%- render_partial('./_translation_toolbar.ejs', { translation }) %>
51
+ </td>
52
+ <% } %>
31
53
  </tr>
32
54
  <% }) %>
33
55
  </tbody>