@raketa-cloud/admin 1.8.16 → 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.16",
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,36 +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
- const WORKFLOWS_MAP = {
12
- lokalise_to_ai_rt_hum: "Lokalise: AI translation & Human review",
13
- lokalise_tt_hum: "Lokalise: Human translation",
14
- lokalise_tt_ai: "Lokalise: AI translation",
15
- };
16
-
17
1
  async function index(ctx) {
18
2
  const items = await ctx.client.contentTasks.list(ctx.query);
19
3
  const editors = await ctx.client.contentTasks.editors();
20
- const resourceRoutes = ctx.globalConfig.resourceRoutes;
21
-
22
- if (!resourceRoutes) {
23
- throw new Error("Missing resourceRoutes in the admin config!");
24
- }
25
4
 
26
5
  return {
27
6
  assigns: {
28
7
  items,
29
8
  editors: editors.list || [],
30
- buildResourceUrl: (resource) =>
31
- buildResourceUrl(resource, resourceRoutes),
32
- workflowsMap: (workflowType) =>
33
- WORKFLOWS_MAP[workflowType] || "Unknown workflow",
34
9
  },
35
10
  };
36
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
- <%= workflowsMap(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">
@@ -102,6 +102,7 @@
102
102
  <ul>
103
103
  <li>Content is required to start a Lokalise task.</li>
104
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>
105
106
  </ul>
106
107
  </div>
107
108
  </div>
@@ -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>