@raketa-cloud/admin 1.10.1 → 1.10.4

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.
@@ -1525,6 +1525,7 @@ select {
1525
1525
 
1526
1526
  .page-title {
1527
1527
  margin-top: 1.5rem;
1528
+ margin-bottom: 1.5rem;
1528
1529
  display: flex;
1529
1530
  align-items: center;
1530
1531
  justify-content: space-between;
@@ -1626,6 +1627,10 @@ select {
1626
1627
  position: static;
1627
1628
  }
1628
1629
 
1630
+ .fixed {
1631
+ position: fixed;
1632
+ }
1633
+
1629
1634
  .absolute {
1630
1635
  position: absolute;
1631
1636
  }
@@ -1642,6 +1647,11 @@ select {
1642
1647
  inset: 0px;
1643
1648
  }
1644
1649
 
1650
+ .inset-x-0 {
1651
+ left: 0px;
1652
+ right: 0px;
1653
+ }
1654
+
1645
1655
  .-right-0 {
1646
1656
  right: -0px;
1647
1657
  }
@@ -1662,6 +1672,10 @@ select {
1662
1672
  top: -0.125rem;
1663
1673
  }
1664
1674
 
1675
+ .bottom-0 {
1676
+ bottom: 0px;
1677
+ }
1678
+
1665
1679
  .bottom-20 {
1666
1680
  bottom: 5rem;
1667
1681
  }
@@ -1863,6 +1877,10 @@ select {
1863
1877
  height: 2rem;
1864
1878
  }
1865
1879
 
1880
+ .h-12 {
1881
+ height: 3rem;
1882
+ }
1883
+
1866
1884
  .h-2 {
1867
1885
  height: 0.5rem;
1868
1886
  }
@@ -1908,6 +1926,10 @@ select {
1908
1926
  max-height: 100vh;
1909
1927
  }
1910
1928
 
1929
+ .min-h-0 {
1930
+ min-height: 0px;
1931
+ }
1932
+
1911
1933
  .w-1 {
1912
1934
  width: 0.25rem;
1913
1935
  }
@@ -1986,6 +2008,22 @@ select {
1986
2008
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1987
2009
  }
1988
2010
 
2011
+ @keyframes bounce {
2012
+ 0%, 100% {
2013
+ transform: translateY(-25%);
2014
+ animation-timing-function: cubic-bezier(0.8,0,1,1);
2015
+ }
2016
+
2017
+ 50% {
2018
+ transform: none;
2019
+ animation-timing-function: cubic-bezier(0,0,0.2,1);
2020
+ }
2021
+ }
2022
+
2023
+ .animate-bounce {
2024
+ animation: bounce 1s infinite;
2025
+ }
2026
+
1989
2027
  @keyframes ping {
1990
2028
  75%, 100% {
1991
2029
  transform: scale(2);
@@ -2035,6 +2073,10 @@ select {
2035
2073
  align-items: flex-start;
2036
2074
  }
2037
2075
 
2076
+ .items-end {
2077
+ align-items: flex-end;
2078
+ }
2079
+
2038
2080
  .items-center {
2039
2081
  align-items: center;
2040
2082
  }
@@ -2263,6 +2305,20 @@ select {
2263
2305
  background-color: rgb(250 204 21 / var(--tw-bg-opacity));
2264
2306
  }
2265
2307
 
2308
+ .bg-gradient-to-t {
2309
+ background-image: linear-gradient(to top, var(--tw-gradient-stops));
2310
+ }
2311
+
2312
+ .from-raketa-primary\/15 {
2313
+ --tw-gradient-from: rgba(var(--primary), 0.15) var(--tw-gradient-from-position);
2314
+ --tw-gradient-to: rgba(var(--primary), 0) var(--tw-gradient-to-position);
2315
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
2316
+ }
2317
+
2318
+ .to-transparent {
2319
+ --tw-gradient-to: transparent var(--tw-gradient-to-position);
2320
+ }
2321
+
2266
2322
  .fill-\[\#34a853\] {
2267
2323
  fill: #34a853;
2268
2324
  }
@@ -2334,6 +2390,10 @@ select {
2334
2390
  padding-bottom: 1rem;
2335
2391
  }
2336
2392
 
2393
+ .pb-1 {
2394
+ padding-bottom: 0.25rem;
2395
+ }
2396
+
2337
2397
  .pb-4 {
2338
2398
  padding-bottom: 1rem;
2339
2399
  }
@@ -2530,6 +2590,10 @@ select {
2530
2590
  color: rgb(255 255 255 / var(--tw-text-opacity));
2531
2591
  }
2532
2592
 
2593
+ .opacity-0 {
2594
+ opacity: 0;
2595
+ }
2596
+
2533
2597
  .opacity-10 {
2534
2598
  opacity: 0.1;
2535
2599
  }
@@ -2589,6 +2653,12 @@ select {
2589
2653
  transition-duration: 150ms;
2590
2654
  }
2591
2655
 
2656
+ .transition-opacity {
2657
+ transition-property: opacity;
2658
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2659
+ transition-duration: 150ms;
2660
+ }
2661
+
2592
2662
  .duration-150 {
2593
2663
  transition-duration: 150ms;
2594
2664
  }
@@ -2601,6 +2671,14 @@ select {
2601
2671
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2602
2672
  }
2603
2673
 
2674
+ .scrollbar-hidden {
2675
+ scrollbar-width: none;
2676
+ }
2677
+
2678
+ .scrollbar-hidden::-webkit-scrollbar {
2679
+ display: none;
2680
+ }
2681
+
2604
2682
  :root {
2605
2683
  --background: 255, 255, 255;
2606
2684
  --foreground: 0, 0, 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.10.1",
3
+ "version": "1.10.4",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -24,26 +24,21 @@ export const buildResourceUrl = (resource, globalConfig) => {
24
24
  return `/${locale}/${route}/${id}`;
25
25
  };
26
26
 
27
- export const hasUnattachedKeys = (task) => {
28
- if (!task) return false;
27
+ export const unattachedKeys = (task) => {
28
+ const keys = task?.lokaliseWorkflow?.settings?.unattached_keys;
29
+ const values = keys?.names || keys?.ids;
29
30
 
30
- const unattachedKeys = task.lokaliseWorkflow?.settings?.unattached_keys;
31
- const ids = unattachedKeys?.ids;
31
+ if (Array.isArray(values)) return values;
32
32
 
33
- if (Array.isArray(ids)) {
34
- return ids.length > 0;
35
- }
36
-
37
- return Boolean(ids);
33
+ return values ? [values] : [];
38
34
  };
39
35
 
40
- export const unattachedKeyIds = (task) => {
41
- const unattachedKeys = task?.lokaliseWorkflow?.settings?.unattached_keys;
42
- const ids = unattachedKeys?.ids;
36
+ export const hasUnattachedKeys = (task) => unattachedKeys(task).length > 0;
43
37
 
44
- if (Array.isArray(ids)) {
45
- return ids.join(", ");
46
- }
38
+ export const unattachedKeyUrl = (task, key) => {
39
+ const keys = task?.lokaliseWorkflow?.settings?.unattached_keys;
40
+
41
+ if (!keys?.names || !keys.url) return null;
47
42
 
48
- return ids ? String(ids) : "";
43
+ return `${keys.url}${encodeURIComponent(key)}`;
49
44
  };
@@ -319,7 +319,7 @@ body {
319
319
  }
320
320
 
321
321
  .page-title {
322
- @apply flex items-center justify-between mt-6;
322
+ @apply flex items-center justify-between my-6;
323
323
  }
324
324
 
325
325
  .actions-toolbar {
@@ -359,6 +359,16 @@ body {
359
359
  }
360
360
  }
361
361
 
362
+ @layer utilities {
363
+ .scrollbar-hidden {
364
+ scrollbar-width: none;
365
+ }
366
+
367
+ .scrollbar-hidden::-webkit-scrollbar {
368
+ display: none;
369
+ }
370
+ }
371
+
362
372
  .space-the-final-frontier {
363
373
  position: fixed;
364
374
  top: 50%;
@@ -2,7 +2,8 @@ import {
2
2
  workflowsMap,
3
3
  buildResourceUrl,
4
4
  hasUnattachedKeys,
5
- unattachedKeyIds,
5
+ unattachedKeys,
6
+ unattachedKeyUrl,
6
7
  } from "../../helpers/content_tasks_helper";
7
8
 
8
9
  describe("content_tasks_helper", () => {
@@ -114,46 +115,89 @@ describe("content_tasks_helper", () => {
114
115
  ).toBe(false);
115
116
  });
116
117
 
117
- test("returns true when task is in progress and has unattached key ids", () => {
118
+ test("returns true when the task has unattached key ids or names", () => {
118
119
  expect(
119
120
  hasUnattachedKeys({
120
- completedAt: null,
121
- failedAt: null,
122
121
  lokaliseWorkflow: {
123
122
  settings: { unattached_keys: { ids: [101, 102] } },
124
123
  },
125
124
  }),
126
125
  ).toBe(true);
126
+ expect(
127
+ hasUnattachedKeys({
128
+ lokaliseWorkflow: {
129
+ settings: { unattached_keys: { names: ["key_a"] } },
130
+ },
131
+ }),
132
+ ).toBe(true);
127
133
  });
128
134
  });
129
135
 
130
- describe("unattachedKeyIds", () => {
131
- test("returns empty string when no unattached keys exist", () => {
132
- expect(unattachedKeyIds(null)).toBe("");
133
- expect(unattachedKeyIds({})).toBe("");
134
- expect(
135
- unattachedKeyIds({
136
- lokaliseWorkflow: { settings: { unattached_keys: {} } },
137
- }),
138
- ).toBe("");
136
+ describe("unattachedKeys", () => {
137
+ test("returns an empty array when no unattached keys exist", () => {
138
+ expect(unattachedKeys(null)).toEqual([]);
139
+ expect(unattachedKeys({})).toEqual([]);
139
140
  });
140
141
 
141
- test("returns comma separated string of ids", () => {
142
+ test("prefers names and falls back to ids", () => {
142
143
  expect(
143
- unattachedKeyIds({
144
+ unattachedKeys({
144
145
  lokaliseWorkflow: {
145
- settings: { unattached_keys: { ids: [101, 102, 103] } },
146
+ settings: {
147
+ unattached_keys: { names: ["key_a", "key_b"], ids: [101, 102] },
148
+ },
146
149
  },
147
150
  }),
148
- ).toBe("101, 102, 103");
149
-
151
+ ).toEqual(["key_a", "key_b"]);
150
152
  expect(
151
- unattachedKeyIds({
153
+ unattachedKeys({
152
154
  lokaliseWorkflow: {
153
- settings: { unattached_keys: { ids: ["key_a", "key_b"] } },
155
+ settings: { unattached_keys: { ids: [101, 102] } },
154
156
  },
155
157
  }),
156
- ).toBe("key_a, key_b");
158
+ ).toEqual([101, 102]);
159
+ });
160
+ });
161
+
162
+ describe("unattachedKeyUrl", () => {
163
+ const task = {
164
+ lokaliseWorkflow: {
165
+ settings: {
166
+ unattached_keys: {
167
+ names: ["key one"],
168
+ url: "https://app.lokalise.com/project/123/?search=",
169
+ },
170
+ },
171
+ },
172
+ };
173
+
174
+ test("builds a Lokalise URL for a key name", () => {
175
+ expect(unattachedKeyUrl(task, "key one")).toBe(
176
+ "https://app.lokalise.com/project/123/?search=key%20one",
177
+ );
178
+ });
179
+
180
+ test("returns null for legacy ids or when the URL is missing", () => {
181
+ expect(
182
+ unattachedKeyUrl(
183
+ {
184
+ lokaliseWorkflow: {
185
+ settings: { unattached_keys: { ids: [101] } },
186
+ },
187
+ },
188
+ 101,
189
+ ),
190
+ ).toBeNull();
191
+ expect(
192
+ unattachedKeyUrl(
193
+ {
194
+ lokaliseWorkflow: {
195
+ settings: { unattached_keys: { names: ["key_a"] } },
196
+ },
197
+ },
198
+ "key_a",
199
+ ),
200
+ ).toBeNull();
157
201
  });
158
202
  });
159
203
  });
@@ -66,7 +66,15 @@
66
66
 
67
67
  <% if (isMarked) { %>
68
68
  <div class="body-3 text-raketa-foreground/50">
69
- <%= content_tasks.unattachedKeyIds(task) %>
69
+ <% content_tasks.unattachedKeys(task).forEach((key, index) => { %>
70
+ <% if (index > 0) { %>, <% } %>
71
+ <% const keyUrl = content_tasks.unattachedKeyUrl(task, key) %>
72
+ <% if (keyUrl) { %>
73
+ <a href="<%= keyUrl %>" target="_blank" class="btn-text"><%= key %></a>
74
+ <% } else { %>
75
+ <%= key %>
76
+ <% } %>
77
+ <% }) %>
70
78
  </div>
71
79
  <% } %>
72
80
  </td>
@@ -64,7 +64,8 @@
64
64
  </svg>
65
65
  </button>
66
66
 
67
- <div class="my-4">
67
+ <div class="relative flex-1 min-h-0">
68
+ <div id="sidebar-menu" class="h-full py-4 overflow-y-auto scrollbar-hidden">
68
69
  <% _context.layout.sidebar.forEach((menu, idx) => { %>
69
70
  <% if (!menu) { %>
70
71
  <% } else if (menu.submenus) { %>
@@ -77,7 +78,7 @@
77
78
  </div>
78
79
  </div>
79
80
 
80
- <div class="absolute z-50 top-0 left-[64px] w-[250px] border-l bg-[#F7F7FC] shadow-lg hidden group-hover/item:block">
81
+ <div data-sidebar-flyout class="fixed z-50 left-[64px] w-[250px] border-l bg-[#F7F7FC] shadow-lg hidden group-hover/item:block">
81
82
  <% menu.submenus.forEach(submenu => { %>
82
83
  <a href="<%= `/${_context.locale}${submenu.link}` %>" title="<%= submenu.label %>" class="block py-3 px-4 body-2 <%= _context.path.includes(submenu.link) ? "text-raketa-foreground" : "text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300" %>"><%= submenu.label %></a>
83
84
  <% }) %>
@@ -132,6 +133,13 @@
132
133
 
133
134
  </div>
134
135
 
136
+ <div id="sidebar-scroll-hint" class="pointer-events-none absolute inset-x-0 bottom-0 h-12 flex items-end justify-center pb-1 bg-gradient-to-t from-raketa-primary/15 to-transparent text-raketa-primary transition-opacity duration-300 opacity-0">
137
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 animate-bounce">
138
+ <path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
139
+ </svg>
140
+ </div>
141
+ </div>
142
+
135
143
  <div class="group/item">
136
144
  <a href="/logout" class="relative flex items-center gap-3 px-5 py-4 border-t border-neutral-300 text-raketa-primary hover:bg-raketa-primary hover:text-white group-[.is-active]/item:bg-raketa-primary group-[.is-active]/item:text-white dark:border-neutral-700">
137
145
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6 opacity-100 group-[.expanded]:opacity-50">
@@ -165,6 +173,50 @@
165
173
  toggleEl.addEventListener('click', handleToggle);
166
174
  </script>
167
175
 
176
+ <script>
177
+ (() => {
178
+ const menuEl = document.getElementById('sidebar-menu');
179
+ const hintEl = document.getElementById('sidebar-scroll-hint');
180
+ let hoveredItem = null;
181
+
182
+ const updateHint = () => {
183
+ const hasMore = menuEl.scrollHeight - menuEl.clientHeight > 1;
184
+ hintEl.classList.toggle('opacity-0', !hasMore || menuEl.scrollTop > 0);
185
+ };
186
+
187
+ // Flyouts are position: fixed so the scrollable menu doesn't clip them
188
+ const positionFlyout = (item) => {
189
+ const flyout = item.querySelector('[data-sidebar-flyout]');
190
+ if (!flyout) return;
191
+
192
+ const { top } = item.getBoundingClientRect();
193
+ const maxTop = window.innerHeight - flyout.offsetHeight;
194
+ flyout.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
195
+ };
196
+
197
+ menuEl.querySelectorAll('[data-sidebar-flyout]').forEach((flyout) => {
198
+ const item = flyout.parentElement;
199
+
200
+ item.addEventListener('mouseenter', () => {
201
+ hoveredItem = item;
202
+ positionFlyout(item);
203
+ });
204
+ item.addEventListener('mouseleave', () => {
205
+ hoveredItem = null;
206
+ });
207
+ });
208
+
209
+ menuEl.addEventListener('scroll', () => {
210
+ updateHint();
211
+ if (hoveredItem) positionFlyout(hoveredItem);
212
+ }, { passive: true });
213
+
214
+ const resizeObserver = new ResizeObserver(updateHint);
215
+ resizeObserver.observe(menuEl);
216
+ Array.from(menuEl.children).forEach((child) => resizeObserver.observe(child));
217
+ })();
218
+ </script>
219
+
168
220
  <script>
169
221
  const TIMEZONES = JSON.parse("<%= JSON.stringify(_context.timezones) %>".replaceAll("&#34;", '"'))
170
222
  const TIME_LOCALE = JSON.parse("<%= JSON.stringify(_context.timeLocale || 'en-GB') %>".replaceAll("&#34;", '"'))