@raketa-cloud/admin 1.9.5 → 1.9.7
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 +4 -34
- package/package.json +2 -2
- package/src/cloud/Resources.js +9 -21
- package/src/cloud/documents/resourcesDocuments.js +1 -10
- package/src/helpers/content_tasks_helper.js +24 -0
- package/src/magicPreview.js +66 -9
- package/src/tests/helpers/content_tasks_helper.test.js +105 -0
- package/src/tests/magicPreview.test.js +79 -0
- package/src/views/_shared/_content_tasks.ejs +26 -1
package/dist/application.css
CHANGED
|
@@ -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 {
|
|
@@ -3557,6 +3523,10 @@ body {
|
|
|
3557
3523
|
background-color: rgb(124 45 18 / 0.5);
|
|
3558
3524
|
}
|
|
3559
3525
|
|
|
3526
|
+
.dark\:bg-orange-950\/40:where(.dark, .dark *) {
|
|
3527
|
+
background-color: rgb(67 20 7 / 0.4);
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3560
3530
|
.dark\:bg-raketa-primary\/15:where(.dark, .dark *) {
|
|
3561
3531
|
background-color: rgba(var(--primary), 0.15);
|
|
3562
3532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raketa-cloud/admin",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.7",
|
|
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.
|
|
40
|
+
"hono": "4.13.7",
|
|
41
41
|
"lru-cache": "11.5.2"
|
|
42
42
|
},
|
|
43
43
|
"overrides": {
|
package/src/cloud/Resources.js
CHANGED
|
@@ -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:
|
|
169
|
-
locale:
|
|
170
|
-
resourceType:
|
|
168
|
+
slug: previewParams.slug,
|
|
169
|
+
locale: previewParams.locale,
|
|
170
|
+
resourceType: previewParams.resourceType,
|
|
171
171
|
},
|
|
172
172
|
this.getConfig(),
|
|
173
173
|
),
|
|
@@ -200,23 +200,11 @@ export default class Resources extends RaketaClient {
|
|
|
200
200
|
translations,
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
slug: paramsCopy.slug,
|
|
209
|
-
locale: sourceLocale || locales[0],
|
|
210
|
-
resourceType,
|
|
211
|
-
},
|
|
212
|
-
this.getConfig(),
|
|
213
|
-
),
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
if (workflow.due_date) {
|
|
217
|
-
input.workflow.dueDate = toUTCISO8601String(workflow.due_date);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
203
|
+
this.appendWorkflow(input, workflow, {
|
|
204
|
+
slug: paramsCopy.slug,
|
|
205
|
+
locale: sourceLocale,
|
|
206
|
+
resourceType,
|
|
207
|
+
});
|
|
220
208
|
|
|
221
209
|
const {
|
|
222
210
|
multiTranslateResource: { resources, errors, localeErrors },
|
|
@@ -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
|
}
|
|
@@ -23,3 +23,27 @@ export const buildResourceUrl = (resource, globalConfig) => {
|
|
|
23
23
|
|
|
24
24
|
return `/${locale}/${route}/${id}`;
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
export const hasUnattachedKeys = (task) => {
|
|
28
|
+
if (!task || task.completedAt || task.failedAt) return false;
|
|
29
|
+
|
|
30
|
+
const unattachedKeys = task.lokaliseWorkflow?.settings?.unattached_keys;
|
|
31
|
+
const ids = unattachedKeys?.ids;
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(ids)) {
|
|
34
|
+
return ids.length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Boolean(ids);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const unattachedKeyIds = (task) => {
|
|
41
|
+
const unattachedKeys = task?.lokaliseWorkflow?.settings?.unattached_keys;
|
|
42
|
+
const ids = unattachedKeys?.ids;
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(ids)) {
|
|
45
|
+
return ids.join(", ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ids ? String(ids) : "";
|
|
49
|
+
};
|
package/src/magicPreview.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
44
|
-
}
|
|
78
|
+
const rawToken = Buffer.concat([iv, tag, encrypted]).toString("base64url");
|
|
45
79
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
80
|
+
return chunkToken(rawToken);
|
|
81
|
+
}
|
|
50
82
|
|
|
83
|
+
function decipherPayload(rawToken, key) {
|
|
51
84
|
try {
|
|
52
|
-
const
|
|
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,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
workflowsMap,
|
|
3
3
|
buildResourceUrl,
|
|
4
|
+
hasUnattachedKeys,
|
|
5
|
+
unattachedKeyIds,
|
|
4
6
|
} from "../../helpers/content_tasks_helper";
|
|
5
7
|
|
|
6
8
|
describe("content_tasks_helper", () => {
|
|
@@ -65,4 +67,107 @@ describe("content_tasks_helper", () => {
|
|
|
65
67
|
);
|
|
66
68
|
});
|
|
67
69
|
});
|
|
70
|
+
|
|
71
|
+
describe("hasUnattachedKeys", () => {
|
|
72
|
+
test("returns false when task is null or undefined", () => {
|
|
73
|
+
expect(hasUnattachedKeys(null)).toBe(false);
|
|
74
|
+
expect(hasUnattachedKeys(undefined)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns false when task is completed or failed", () => {
|
|
78
|
+
expect(
|
|
79
|
+
hasUnattachedKeys({
|
|
80
|
+
completedAt: "2024-01-01T00:00:00Z",
|
|
81
|
+
lokaliseWorkflow: { settings: { unattached_keys: { ids: [1, 2] } } },
|
|
82
|
+
}),
|
|
83
|
+
).toBe(false);
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
hasUnattachedKeys({
|
|
87
|
+
failedAt: "2024-01-01T00:00:00Z",
|
|
88
|
+
lokaliseWorkflow: { settings: { unattached_keys: { ids: [1, 2] } } },
|
|
89
|
+
}),
|
|
90
|
+
).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("returns false when task has no lokaliseWorkflow or settings", () => {
|
|
94
|
+
expect(hasUnattachedKeys({ completedAt: null, failedAt: null })).toBe(
|
|
95
|
+
false,
|
|
96
|
+
);
|
|
97
|
+
expect(
|
|
98
|
+
hasUnattachedKeys({
|
|
99
|
+
completedAt: null,
|
|
100
|
+
failedAt: null,
|
|
101
|
+
lokaliseWorkflow: {},
|
|
102
|
+
}),
|
|
103
|
+
).toBe(false);
|
|
104
|
+
expect(
|
|
105
|
+
hasUnattachedKeys({
|
|
106
|
+
completedAt: null,
|
|
107
|
+
failedAt: null,
|
|
108
|
+
lokaliseWorkflow: { settings: {} },
|
|
109
|
+
}),
|
|
110
|
+
).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns false when unattached_keys has empty ids array or null", () => {
|
|
114
|
+
expect(
|
|
115
|
+
hasUnattachedKeys({
|
|
116
|
+
completedAt: null,
|
|
117
|
+
failedAt: null,
|
|
118
|
+
lokaliseWorkflow: { settings: { unattached_keys: { ids: [] } } },
|
|
119
|
+
}),
|
|
120
|
+
).toBe(false);
|
|
121
|
+
|
|
122
|
+
expect(
|
|
123
|
+
hasUnattachedKeys({
|
|
124
|
+
completedAt: null,
|
|
125
|
+
failedAt: null,
|
|
126
|
+
lokaliseWorkflow: { settings: { unattached_keys: null } },
|
|
127
|
+
}),
|
|
128
|
+
).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("returns true when task is in progress and has unattached key ids", () => {
|
|
132
|
+
expect(
|
|
133
|
+
hasUnattachedKeys({
|
|
134
|
+
completedAt: null,
|
|
135
|
+
failedAt: null,
|
|
136
|
+
lokaliseWorkflow: {
|
|
137
|
+
settings: { unattached_keys: { ids: [101, 102] } },
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("unattachedKeyIds", () => {
|
|
145
|
+
test("returns empty string when no unattached keys exist", () => {
|
|
146
|
+
expect(unattachedKeyIds(null)).toBe("");
|
|
147
|
+
expect(unattachedKeyIds({})).toBe("");
|
|
148
|
+
expect(
|
|
149
|
+
unattachedKeyIds({
|
|
150
|
+
lokaliseWorkflow: { settings: { unattached_keys: {} } },
|
|
151
|
+
}),
|
|
152
|
+
).toBe("");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("returns comma separated string of ids", () => {
|
|
156
|
+
expect(
|
|
157
|
+
unattachedKeyIds({
|
|
158
|
+
lokaliseWorkflow: {
|
|
159
|
+
settings: { unattached_keys: { ids: [101, 102, 103] } },
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
).toBe("101, 102, 103");
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
unattachedKeyIds({
|
|
166
|
+
lokaliseWorkflow: {
|
|
167
|
+
settings: { unattached_keys: { ids: ["key_a", "key_b"] } },
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
).toBe("key_a, key_b");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
68
173
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMagicPreviewAddress,
|
|
3
|
+
chunkToken,
|
|
3
4
|
decipherMagicPreviewToken,
|
|
4
5
|
generateMagicPreviewToken,
|
|
6
|
+
unchunkToken,
|
|
5
7
|
} from "../magicPreview";
|
|
6
8
|
|
|
7
9
|
describe("magicPreview helper", () => {
|
|
@@ -104,6 +106,13 @@ describe("magicPreview helper", () => {
|
|
|
104
106
|
expect(typeof token).toBe("string");
|
|
105
107
|
expect(token).not.toContain("my-first-ebook");
|
|
106
108
|
|
|
109
|
+
// Verify token contains dashes breaking it into chunks of at most 40 characters
|
|
110
|
+
const chunks = token.split("-");
|
|
111
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
112
|
+
for (const chunk of chunks) {
|
|
113
|
+
expect(chunk.length).toBeLessThanOrEqual(40);
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
const payload = decipherMagicPreviewToken(token, config);
|
|
108
117
|
expect(payload).not.toBeNull();
|
|
109
118
|
expect(payload.slug).toBe("my-first-ebook");
|
|
@@ -111,9 +120,40 @@ describe("magicPreview helper", () => {
|
|
|
111
120
|
expect(payload.locale).toBe("en");
|
|
112
121
|
});
|
|
113
122
|
|
|
123
|
+
it("deciphers legacy unchunked tokens without dashes", () => {
|
|
124
|
+
// Manually produce a legacy unchunked token
|
|
125
|
+
const {
|
|
126
|
+
createCipheriv,
|
|
127
|
+
createHash,
|
|
128
|
+
randomBytes,
|
|
129
|
+
} = require("node:crypto");
|
|
130
|
+
const key = createHash("sha256")
|
|
131
|
+
.update(config.secrets.crypto_secret)
|
|
132
|
+
.digest();
|
|
133
|
+
const iv = randomBytes(12);
|
|
134
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
135
|
+
const payload = {
|
|
136
|
+
slug: "legacy-post",
|
|
137
|
+
resourceType: "post",
|
|
138
|
+
locale: "de",
|
|
139
|
+
};
|
|
140
|
+
const encrypted = Buffer.concat([
|
|
141
|
+
cipher.update(JSON.stringify(payload), "utf8"),
|
|
142
|
+
cipher.final(),
|
|
143
|
+
]);
|
|
144
|
+
const tag = cipher.getAuthTag();
|
|
145
|
+
const legacyToken = Buffer.concat([iv, tag, encrypted]).toString(
|
|
146
|
+
"base64url",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const deciphered = decipherMagicPreviewToken(legacyToken, config);
|
|
150
|
+
expect(deciphered).toEqual(payload);
|
|
151
|
+
});
|
|
152
|
+
|
|
114
153
|
it("returns null on invalid / tampered token", () => {
|
|
115
154
|
expect(decipherMagicPreviewToken("", config)).toBeNull();
|
|
116
155
|
expect(decipherMagicPreviewToken(null, config)).toBeNull();
|
|
156
|
+
expect(decipherMagicPreviewToken(123, config)).toBeNull();
|
|
117
157
|
expect(decipherMagicPreviewToken("invalid-token", config)).toBeNull();
|
|
118
158
|
|
|
119
159
|
const token = generateMagicPreviewToken(
|
|
@@ -124,4 +164,43 @@ describe("magicPreview helper", () => {
|
|
|
124
164
|
expect(decipherMagicPreviewToken(tampered, config)).toBeNull();
|
|
125
165
|
});
|
|
126
166
|
});
|
|
167
|
+
|
|
168
|
+
describe("chunkToken and unchunkToken helpers", () => {
|
|
169
|
+
it("handles non-string or falsy input gracefully", () => {
|
|
170
|
+
expect(chunkToken(null)).toBeNull();
|
|
171
|
+
expect(chunkToken(undefined)).toBeUndefined();
|
|
172
|
+
expect(chunkToken("")).toBe("");
|
|
173
|
+
expect(unchunkToken(null)).toBeNull();
|
|
174
|
+
expect(unchunkToken(undefined)).toBeUndefined();
|
|
175
|
+
expect(unchunkToken("")).toBe("");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("chunks string into 40-char segments joined by dashes", () => {
|
|
179
|
+
const input = "a".repeat(40) + "b".repeat(40) + "c".repeat(15);
|
|
180
|
+
const chunked = chunkToken(input, 40);
|
|
181
|
+
|
|
182
|
+
expect(chunked).toBe(
|
|
183
|
+
"a".repeat(40) + "-" + "b".repeat(40) + "-" + "c".repeat(15),
|
|
184
|
+
);
|
|
185
|
+
expect(unchunkToken(chunked, 40)).toBe(input);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("leaves string <= chunkSize unchanged", () => {
|
|
189
|
+
const input = "a".repeat(35);
|
|
190
|
+
|
|
191
|
+
expect(chunkToken(input, 40)).toBe(input);
|
|
192
|
+
expect(unchunkToken(input, 40)).toBe(input);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("handles strings with existing dashes inside chunks", () => {
|
|
196
|
+
const input = "abc-def-123".repeat(10);
|
|
197
|
+
const chunked = chunkToken(input, 40);
|
|
198
|
+
|
|
199
|
+
expect(chunked).toBe(
|
|
200
|
+
"abc-def-123abc-def-123abc-def-123abc-def--123abc-def-123abc-def-123abc-def-123abc--def-123abc-def-123abc-def-123",
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(unchunkToken(chunked, 40)).toBe(input);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
127
206
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<th>Workflow</th>
|
|
10
10
|
<th>Started at</th>
|
|
11
11
|
<% if (showAllDetails) { %>
|
|
12
|
+
<th>Due date</th>
|
|
12
13
|
<th>Finished at</th>
|
|
13
14
|
<% } %>
|
|
14
15
|
<th>Status</th>
|
|
@@ -18,8 +19,9 @@
|
|
|
18
19
|
<tbody>
|
|
19
20
|
<% tasks.forEach(task => { %>
|
|
20
21
|
<% const resources = task.resources || []; %>
|
|
22
|
+
<% const isMarked = showAllDetails && content_tasks.hasUnattachedKeys(task); %>
|
|
21
23
|
|
|
22
|
-
<tr>
|
|
24
|
+
<tr class="<%= isMarked ? 'bg-orange-100 dark:bg-orange-950/40' : '' %>">
|
|
23
25
|
<% if (showAllDetails) { %>
|
|
24
26
|
<td class="w-1/3">
|
|
25
27
|
<% if (resources && resources.length > 0) { %>
|
|
@@ -61,6 +63,12 @@
|
|
|
61
63
|
<span class="body-2">
|
|
62
64
|
<%= content_tasks.workflowsMap(task.workflow) %>
|
|
63
65
|
</span>
|
|
66
|
+
|
|
67
|
+
<% if (isMarked) { %>
|
|
68
|
+
<div class="body-3 text-raketa-foreground/50">
|
|
69
|
+
<%= content_tasks.unattachedKeyIds(task) %>
|
|
70
|
+
</div>
|
|
71
|
+
<% } %>
|
|
64
72
|
</td>
|
|
65
73
|
|
|
66
74
|
<td>
|
|
@@ -84,6 +92,23 @@
|
|
|
84
92
|
</td>
|
|
85
93
|
|
|
86
94
|
<% if (showAllDetails) { %>
|
|
95
|
+
<td>
|
|
96
|
+
<div class="body-3 text-raketa-foreground/50">
|
|
97
|
+
<% const dueDate = task.lokaliseWorkflow?.settings?.due_date %>
|
|
98
|
+
|
|
99
|
+
<% if (dueDate) { %>
|
|
100
|
+
<div class="flex items-center gap-1 mb-0.5">
|
|
101
|
+
<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">
|
|
102
|
+
<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" />
|
|
103
|
+
</svg>
|
|
104
|
+
<time data-pretty-datetime datetime="<%= dueDate %>"><%= utils.formattedDateTime(dueDate) %></time>
|
|
105
|
+
</div>
|
|
106
|
+
<% } else { %>
|
|
107
|
+
<span class="text-raketa-foreground/50">—</span>
|
|
108
|
+
<% } %>
|
|
109
|
+
</div>
|
|
110
|
+
</td>
|
|
111
|
+
|
|
87
112
|
<td>
|
|
88
113
|
<% if (task.completedAt || task.failedAt) { %>
|
|
89
114
|
<div class="body-3 text-raketa-foreground/50">
|