@raketa-cloud/admin 1.7.4 → 1.7.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.
Files changed (38) hide show
  1. package/dist/application.css +23 -0
  2. package/package.json +3 -3
  3. package/src/HonoAdapter.js +7 -3
  4. package/src/cloud/Articles.js +10 -3
  5. package/src/cloud/Events.js +9 -2
  6. package/src/cloud/Pages.js +13 -1
  7. package/src/cloud/documents/articlesDocuments.js +4 -2
  8. package/src/cloud/documents/eventsDocuments.js +4 -2
  9. package/src/cloud/documents/pagesDocuments.js +4 -2
  10. package/src/handlers/_shared/SettingsHandler.js +4 -0
  11. package/src/handlers/articles/ArticlesHandler.js +10 -4
  12. package/src/handlers/articles/ExportsHandler.js +35 -0
  13. package/src/handlers/events/EventsHandler.js +10 -4
  14. package/src/handlers/events/ExportsHandler.js +35 -0
  15. package/src/handlers/pages/ExportsHandler.js +35 -0
  16. package/src/handlers/pages/PagesHandler.js +10 -4
  17. package/src/helpers/CSVExportHelper.js +103 -0
  18. package/src/routeArticlesModule.js +13 -0
  19. package/src/routeEventsModule.js +13 -0
  20. package/src/routePagesModule.js +13 -0
  21. package/src/routeSettings.js +4 -0
  22. package/src/tests/RaketaClient.test.js +636 -0
  23. package/src/tests/handlers/articles/ArticlesHandler.test/index.test.js +18 -2
  24. package/src/tests/handlers/articles/ExportsHandler.test/index.test.js +217 -0
  25. package/src/tests/handlers/events/EventsHandler.test/index.test.js +17 -1
  26. package/src/tests/handlers/events/ExportsHandler.test/index.test.js +212 -0
  27. package/src/tests/handlers/pages/ExportsHandler.test/index.test.js +212 -0
  28. package/src/tests/handlers/pages/PagesHandler.test/index.test.js +17 -1
  29. package/src/tests/helpers/CSVExportHelper.test.js +364 -0
  30. package/src/views/articles/_shared/_export_button.ejs +9 -0
  31. package/src/views/articles/index.ejs +37 -1
  32. package/src/views/events/_shared/_export_button.ejs +9 -0
  33. package/src/views/events/index.ejs +37 -1
  34. package/src/views/layouts/application.ejs +2 -0
  35. package/src/views/pages/_shared/_export_button.ejs +9 -0
  36. package/src/views/pages/index.ejs +37 -1
  37. package/src/views/settings/_nav.ejs +7 -1
  38. package/src/views/settings/variables/edit_form.ejs +178 -0
@@ -0,0 +1,212 @@
1
+ import ExportsHandler from "../../../../handlers/pages/ExportsHandler";
2
+ import Pages from "../../../../cloud/Pages";
3
+
4
+ describe("Pages ExportsHandler", () => {
5
+ function buildCtx(query = {}) {
6
+ return {
7
+ params: {},
8
+ query: query,
9
+ client: {
10
+ pages: new Pages(() => ({}), "token"),
11
+ },
12
+ resourceConfig: {
13
+ pageType: "page",
14
+ },
15
+ remoteConfig: {
16
+ resourceUrl: (item) => item.slug,
17
+ },
18
+ };
19
+ }
20
+
21
+ describe("index (exportCSV)", () => {
22
+ test("exports pages to CSV with default locale", async () => {
23
+ const mockPages = [
24
+ {
25
+ id: 1,
26
+ title: "Page 1",
27
+ slug: "/page-1",
28
+ visibility: "public",
29
+ insertedAt: "2024-01-01",
30
+ updatedAt: "2024-01-02",
31
+ },
32
+ {
33
+ id: 2,
34
+ title: "Page 2",
35
+ slug: "/page-2",
36
+ visibility: "private",
37
+ insertedAt: "2024-01-03",
38
+ updatedAt: "2024-01-04",
39
+ },
40
+ ];
41
+
42
+ const ctx = buildCtx({});
43
+
44
+ const mockedlistAll = jest
45
+ .spyOn(ctx.client.pages, "listAll")
46
+ .mockImplementation(() => ({ list: mockPages }));
47
+
48
+ const mockDate = new Date("2024-08-30T10:30:00");
49
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
50
+
51
+ const result = await ExportsHandler.index(ctx);
52
+
53
+ expect(mockedlistAll).toHaveBeenCalledWith({}, "en", "page");
54
+
55
+ expect(result.text).toContain(
56
+ "id,type,locale,title,URL,visibility,created_at,updated_at",
57
+ );
58
+ expect(result.text).toContain(
59
+ "1,page,en,Page 1,/page-1,public,2024-01-01,2024-01-02",
60
+ );
61
+ expect(result.text).toContain(
62
+ "2,page,en,Page 2,/page-2,private,2024-01-03,2024-01-04",
63
+ );
64
+
65
+ expect(result.headers).toEqual({
66
+ "Content-Type": "text/csv",
67
+ "Content-Disposition": 'attachment; filename="300824_page_export.csv"',
68
+ });
69
+
70
+ global.Date.mockRestore();
71
+ });
72
+
73
+ test("exports pages to CSV with specified locale", async () => {
74
+ const mockPages = [
75
+ {
76
+ id: 1,
77
+ title: "Página 1",
78
+ slug: "/pagina-1",
79
+ visibility: "public",
80
+ insertedAt: "2024-01-01",
81
+ updatedAt: "2024-01-02",
82
+ },
83
+ ];
84
+
85
+ const ctx = buildCtx({ locale: "es" });
86
+
87
+ const mockedlistAll = jest
88
+ .spyOn(ctx.client.pages, "listAll")
89
+ .mockImplementation(() => ({ list: mockPages }));
90
+
91
+ const result = await ExportsHandler.index(ctx);
92
+
93
+ expect(mockedlistAll).toHaveBeenCalledWith(
94
+ { locale: "es" },
95
+ "es",
96
+ "page",
97
+ );
98
+
99
+ expect(result.text).toContain(
100
+ "1,page,es,Página 1,/pagina-1,public,2024-01-01,2024-01-02",
101
+ );
102
+ });
103
+
104
+ test("exports pages with CSV special characters properly escaped", async () => {
105
+ const mockPages = [
106
+ {
107
+ id: 1,
108
+ title: 'Page with, comma and "quotes"',
109
+ slug: "/page-1",
110
+ visibility: "public",
111
+ insertedAt: "2024-01-01",
112
+ updatedAt: "2024-01-02",
113
+ },
114
+ {
115
+ id: 2,
116
+ title: "Page with\nnewline",
117
+ slug: "/page-2",
118
+ visibility: "private",
119
+ insertedAt: "2024-01-03",
120
+ updatedAt: "2024-01-04",
121
+ },
122
+ ];
123
+
124
+ const ctx = buildCtx({});
125
+
126
+ jest
127
+ .spyOn(ctx.client.pages, "listAll")
128
+ .mockImplementation(() => ({ list: mockPages }));
129
+
130
+ const result = await ExportsHandler.index(ctx);
131
+
132
+ expect(result.text).toContain('"Page with, comma and ""quotes"""');
133
+ expect(result.text).toContain('"Page with\nnewline"');
134
+ });
135
+
136
+ test("exports empty list when no pages found", async () => {
137
+ const ctx = buildCtx({});
138
+
139
+ jest
140
+ .spyOn(ctx.client.pages, "listAll")
141
+ .mockImplementation(() => ({ list: [] }));
142
+
143
+ const result = await ExportsHandler.index(ctx);
144
+
145
+ expect(result.text).toBe(
146
+ "id,type,locale,title,URL,visibility,created_at,updated_at",
147
+ );
148
+ });
149
+
150
+ test("passes query parameters to listAll", async () => {
151
+ const query = {
152
+ search: "test",
153
+ visibility: "public",
154
+ locale: "en",
155
+ };
156
+
157
+ const ctx = buildCtx(query);
158
+
159
+ const mockedlistAll = jest
160
+ .spyOn(ctx.client.pages, "listAll")
161
+ .mockImplementation(() => ({ list: [] }));
162
+
163
+ await ExportsHandler.index(ctx);
164
+
165
+ expect(mockedlistAll).toHaveBeenCalledWith(query, "en", "page");
166
+ });
167
+
168
+ test("uses pageType from resourceConfig", async () => {
169
+ const ctx = buildCtx({});
170
+ ctx.resourceConfig.pageType = "custom-page-type";
171
+
172
+ const mockedlistAll = jest
173
+ .spyOn(ctx.client.pages, "listAll")
174
+ .mockImplementation(() => ({ list: [] }));
175
+
176
+ const mockDate = new Date("2024-08-30T10:30:00");
177
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
178
+
179
+ const result = await ExportsHandler.index(ctx);
180
+
181
+ expect(mockedlistAll).toHaveBeenCalledWith({}, "en", "custom-page-type");
182
+
183
+ expect(result.headers["Content-Disposition"]).toContain(
184
+ "custom-page-type_export.csv",
185
+ );
186
+
187
+ global.Date.mockRestore();
188
+ });
189
+
190
+ test("exports all results without pagination", async () => {
191
+ const mockPages = Array.from({ length: 150 }, (_, i) => ({
192
+ id: i + 1,
193
+ title: `Page ${i + 1}`,
194
+ slug: `/page-${i + 1}`,
195
+ visibility: "public",
196
+ insertedAt: "2024-01-01",
197
+ updatedAt: "2024-01-02",
198
+ }));
199
+
200
+ const ctx = buildCtx({});
201
+
202
+ jest
203
+ .spyOn(ctx.client.pages, "listAll")
204
+ .mockImplementation(() => ({ list: mockPages }));
205
+
206
+ const result = await ExportsHandler.index(ctx);
207
+
208
+ const lines = result.text.split("\r\n");
209
+ expect(lines.length).toBe(151);
210
+ });
211
+ });
212
+ });
@@ -6,9 +6,20 @@ import Factory from "../../../support/factory";
6
6
 
7
7
  describe("Pages Handler", () => {
8
8
  function buildCtx() {
9
+ const mockPages = new Pages(() => ({}), "token");
10
+ mockPages.getConfig = jest.fn().mockReturnValue({
11
+ i18n: {
12
+ locales: {
13
+ en: { name: "English", code: "en-gb" },
14
+ es: { name: "Spanish", code: "es" },
15
+ fr: { name: "French", code: "fr" },
16
+ },
17
+ },
18
+ });
19
+
9
20
  return {
10
21
  client: {
11
- pages: new Pages(() => ({}), "token"),
22
+ pages: mockPages,
12
23
  tags: new Tags(() => ({}), "token"),
13
24
  },
14
25
  query: { foo: "bar" },
@@ -45,6 +56,11 @@ describe("Pages Handler", () => {
45
56
  list: [page],
46
57
  pagination: pagination,
47
58
  },
59
+ translations: [
60
+ { name: "English", code: "en" },
61
+ { name: "Spanish", code: "es" },
62
+ { name: "French", code: "fr" },
63
+ ],
48
64
  tags: tags,
49
65
  visibilityOptions: ctx.client.pages.visibilityOptions(),
50
66
  },
@@ -0,0 +1,364 @@
1
+ import CSVExportHelper from "../../helpers/CSVExportHelper";
2
+
3
+ describe("CSVExportHelper", () => {
4
+ describe("escapeCSVValue", () => {
5
+ test("returns empty string for null", () => {
6
+ const result = CSVExportHelper.escapeCSVValue(null);
7
+ expect(result).toBe("");
8
+ });
9
+
10
+ test("returns empty string for undefined", () => {
11
+ const result = CSVExportHelper.escapeCSVValue(undefined);
12
+ expect(result).toBe("");
13
+ });
14
+
15
+ test("returns string as-is for simple values", () => {
16
+ const result = CSVExportHelper.escapeCSVValue("simple text");
17
+ expect(result).toBe("simple text");
18
+ });
19
+
20
+ test("wraps value in quotes and escapes internal quotes when value contains comma", () => {
21
+ const result = CSVExportHelper.escapeCSVValue("text, with comma");
22
+ expect(result).toBe('"text, with comma"');
23
+ });
24
+
25
+ test("wraps value in quotes and escapes internal quotes when value contains quotes", () => {
26
+ const result = CSVExportHelper.escapeCSVValue('text "with" quotes');
27
+ expect(result).toBe('"text ""with"" quotes"');
28
+ });
29
+
30
+ test("wraps value in quotes when value contains newline", () => {
31
+ const result = CSVExportHelper.escapeCSVValue("text\nwith\nnewlines");
32
+ expect(result).toBe('"text\nwith\nnewlines"');
33
+ });
34
+
35
+ test("wraps value in quotes when value contains carriage return", () => {
36
+ const result = CSVExportHelper.escapeCSVValue("text\rwith\rreturns");
37
+ expect(result).toBe('"text\rwith\rreturns"');
38
+ });
39
+
40
+ test("handles values with multiple special characters", () => {
41
+ const result = CSVExportHelper.escapeCSVValue(
42
+ 'text, "with" special\nchars',
43
+ );
44
+ expect(result).toBe('"text, ""with"" special\nchars"');
45
+ });
46
+
47
+ test("converts numbers to strings", () => {
48
+ const result = CSVExportHelper.escapeCSVValue(42);
49
+ expect(result).toBe("42");
50
+ });
51
+
52
+ test("converts booleans to strings", () => {
53
+ const result = CSVExportHelper.escapeCSVValue(true);
54
+ expect(result).toBe("true");
55
+ });
56
+ });
57
+
58
+ describe("formatDataAsCSV", () => {
59
+ test("formats simple data with direct field mapping", () => {
60
+ const data = [
61
+ { id: 1, name: "Item 1" },
62
+ { id: 2, name: "Item 2" },
63
+ ];
64
+ const headers = ["id", "name"];
65
+ const fieldMapping = {
66
+ id: "id",
67
+ name: "name",
68
+ };
69
+
70
+ const result = CSVExportHelper.formatDataAsCSV(
71
+ data,
72
+ headers,
73
+ fieldMapping,
74
+ );
75
+
76
+ expect(result).toBe("id,name\r\n1,Item 1\r\n2,Item 2");
77
+ });
78
+
79
+ test("formats data with function field mapping", () => {
80
+ const data = [
81
+ { id: 1, firstName: "John", lastName: "Doe" },
82
+ { id: 2, firstName: "Jane", lastName: "Smith" },
83
+ ];
84
+ const headers = ["id", "fullName"];
85
+ const fieldMapping = {
86
+ id: "id",
87
+ fullName: (item) => `${item.firstName} ${item.lastName}`,
88
+ };
89
+
90
+ const result = CSVExportHelper.formatDataAsCSV(
91
+ data,
92
+ headers,
93
+ fieldMapping,
94
+ );
95
+
96
+ expect(result).toBe("id,fullName\r\n1,John Doe\r\n2,Jane Smith");
97
+ });
98
+
99
+ test("formats data with nested field mapping", () => {
100
+ const data = [
101
+ { id: 1, author: { name: "John Doe" } },
102
+ { id: 2, author: { name: "Jane Smith" } },
103
+ ];
104
+ const headers = ["id", "author"];
105
+ const fieldMapping = {
106
+ id: "id",
107
+ author: "author.name",
108
+ };
109
+
110
+ const result = CSVExportHelper.formatDataAsCSV(
111
+ data,
112
+ headers,
113
+ fieldMapping,
114
+ );
115
+
116
+ expect(result).toBe("id,author\r\n1,John Doe\r\n2,Jane Smith");
117
+ });
118
+
119
+ test("handles missing nested properties", () => {
120
+ const data = [
121
+ { id: 1, author: { name: "John Doe" } },
122
+ { id: 2, author: {} },
123
+ { id: 3 },
124
+ ];
125
+ const headers = ["id", "author"];
126
+ const fieldMapping = {
127
+ id: "id",
128
+ author: "author.name",
129
+ };
130
+
131
+ const result = CSVExportHelper.formatDataAsCSV(
132
+ data,
133
+ headers,
134
+ fieldMapping,
135
+ );
136
+
137
+ expect(result).toBe("id,author\r\n1,John Doe\r\n2,\r\n3,");
138
+ });
139
+
140
+ test("handles missing direct properties", () => {
141
+ const data = [{ id: 1, title: "Title 1" }, { id: 2 }];
142
+ const headers = ["id", "title"];
143
+ const fieldMapping = {
144
+ id: "id",
145
+ title: "title",
146
+ };
147
+
148
+ const result = CSVExportHelper.formatDataAsCSV(
149
+ data,
150
+ headers,
151
+ fieldMapping,
152
+ );
153
+
154
+ expect(result).toBe("id,title\r\n1,Title 1\r\n2,");
155
+ });
156
+
157
+ test("properly escapes CSV special characters in data", () => {
158
+ const data = [
159
+ { id: 1, title: 'Title with, comma and "quotes"' },
160
+ { id: 2, title: "Title with\nnewline" },
161
+ ];
162
+ const headers = ["id", "title"];
163
+ const fieldMapping = {
164
+ id: "id",
165
+ title: "title",
166
+ };
167
+
168
+ const result = CSVExportHelper.formatDataAsCSV(
169
+ data,
170
+ headers,
171
+ fieldMapping,
172
+ );
173
+
174
+ expect(result).toBe(
175
+ 'id,title\r\n1,"Title with, comma and ""quotes"""\r\n2,"Title with\nnewline"',
176
+ );
177
+ });
178
+
179
+ test("formats empty data array", () => {
180
+ const data = [];
181
+ const headers = ["id", "name"];
182
+ const fieldMapping = {
183
+ id: "id",
184
+ name: "name",
185
+ };
186
+
187
+ const result = CSVExportHelper.formatDataAsCSV(
188
+ data,
189
+ headers,
190
+ fieldMapping,
191
+ );
192
+
193
+ expect(result).toBe("id,name");
194
+ });
195
+
196
+ test("formats data with standard field mapping", () => {
197
+ const mockRemoteConfig = {
198
+ resourceUrl: (item, type, locale) => `/${locale}/${type}/${item.id}`,
199
+ };
200
+
201
+ const data = [
202
+ {
203
+ id: 1,
204
+ title: "Test Article",
205
+ slug: "/test-article",
206
+ visibility: "public",
207
+ insertedAt: "2024-01-01",
208
+ updatedAt: "2024-01-02",
209
+ },
210
+ ];
211
+ const headers = [...CSVExportHelper.standardHeaders];
212
+ const fieldMapping = CSVExportHelper.standardFieldMapping(
213
+ mockRemoteConfig,
214
+ "en",
215
+ "article",
216
+ );
217
+
218
+ const result = CSVExportHelper.formatDataAsCSV(
219
+ data,
220
+ headers,
221
+ fieldMapping,
222
+ );
223
+
224
+ expect(result).toBe(
225
+ "id,type,locale,title,URL,visibility,created_at,updated_at\r\n" +
226
+ "1,article,en,Test Article,/en/article/1,public,2024-01-01,2024-01-02",
227
+ );
228
+ });
229
+ });
230
+
231
+ describe("generateExportFilename", () => {
232
+ test("generates filename with correct format", () => {
233
+ const mockDate = new Date("2024-08-30T10:30:00");
234
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
235
+
236
+ const result = CSVExportHelper.generateExportFilename("articles");
237
+
238
+ expect(result).toBe("300824_articles_export.csv");
239
+
240
+ global.Date.mockRestore();
241
+ });
242
+
243
+ test("pads single digit day and month with zeros", () => {
244
+ const mockDate = new Date("2024-01-05T10:30:00");
245
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
246
+
247
+ const result = CSVExportHelper.generateExportFilename("pages");
248
+
249
+ expect(result).toBe("050124_pages_export.csv");
250
+
251
+ global.Date.mockRestore();
252
+ });
253
+
254
+ test("uses two-digit year format", () => {
255
+ const mockDate = new Date("2025-12-25T10:30:00");
256
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
257
+
258
+ const result = CSVExportHelper.generateExportFilename("events");
259
+
260
+ expect(result).toBe("251225_events_export.csv");
261
+
262
+ global.Date.mockRestore();
263
+ });
264
+
265
+ test("includes content type in filename", () => {
266
+ const mockDate = new Date("2024-06-15T10:30:00");
267
+ jest.spyOn(global, "Date").mockImplementation(() => mockDate);
268
+
269
+ const result = CSVExportHelper.generateExportFilename("custom-type");
270
+
271
+ expect(result).toBe("150624_custom-type_export.csv");
272
+
273
+ global.Date.mockRestore();
274
+ });
275
+ });
276
+
277
+ describe("standardFieldMapping", () => {
278
+ test("is a function that returns correct field mappings", () => {
279
+ const mockRemoteConfig = {
280
+ resourceUrl: jest.fn(),
281
+ };
282
+ const locale = "en";
283
+ const type = "article";
284
+
285
+ const mapping = CSVExportHelper.standardFieldMapping(
286
+ mockRemoteConfig,
287
+ locale,
288
+ type,
289
+ );
290
+
291
+ expect(mapping).toEqual({
292
+ id: "id",
293
+ type: expect.any(Function),
294
+ locale: expect.any(Function),
295
+ title: "title",
296
+ URL: expect.any(Function),
297
+ visibility: "visibility",
298
+ created_at: "insertedAt",
299
+ updated_at: "updatedAt",
300
+ });
301
+ });
302
+
303
+ test("locale field mapping returns item locale if present, otherwise uses provided locale", () => {
304
+ const mockRemoteConfig = {
305
+ resourceUrl: jest.fn(),
306
+ };
307
+ const defaultLocale = "en";
308
+ const type = "article";
309
+
310
+ const mapping = CSVExportHelper.standardFieldMapping(
311
+ mockRemoteConfig,
312
+ defaultLocale,
313
+ type,
314
+ );
315
+
316
+ const itemWithLocale = { locale: "fr" };
317
+ const itemWithoutLocale = {};
318
+
319
+ expect(mapping.locale(itemWithLocale)).toBe("fr");
320
+ expect(mapping.locale(itemWithoutLocale)).toBe("en");
321
+ });
322
+
323
+ test("URL field mapping calls remoteConfig.resourceUrl with correct parameters", () => {
324
+ const mockRemoteConfig = {
325
+ resourceUrl: jest.fn(
326
+ (item, type, locale) => `testweb.com/${locale}/${type}/${item.id}`,
327
+ ),
328
+ };
329
+ const locale = "en";
330
+ const type = "article";
331
+
332
+ const mapping = CSVExportHelper.standardFieldMapping(
333
+ mockRemoteConfig,
334
+ locale,
335
+ type,
336
+ );
337
+
338
+ const item = { id: 123 };
339
+ const result = mapping.URL(item);
340
+
341
+ expect(mockRemoteConfig.resourceUrl).toHaveBeenCalledWith(
342
+ item,
343
+ type,
344
+ locale,
345
+ );
346
+ expect(result).toBe(`testweb.com/${locale}/${type}/${item.id}`);
347
+ });
348
+ });
349
+
350
+ describe("standardHeaders", () => {
351
+ test("has correct standard headers", () => {
352
+ expect(CSVExportHelper.standardHeaders).toEqual([
353
+ "id",
354
+ "type",
355
+ "locale",
356
+ "title",
357
+ "URL",
358
+ "visibility",
359
+ "created_at",
360
+ "updated_at",
361
+ ]);
362
+ });
363
+ });
364
+ });
@@ -0,0 +1,9 @@
1
+ <% if (Object.keys(_context.query).some(k => k.startsWith('f:')) && items.list.length > 0) { %>
2
+ <%
3
+ const queryString = Object.entries(_context.query)
4
+ .filter(([key, value]) => value !== undefined && value !== null && value !== '')
5
+ .map(([key, value]) => Array.isArray(value) ? value.map(v => `${key}=${encodeURIComponent(v)}`).join('&') : `${key}=${encodeURIComponent(value)}`)
6
+ .join('&');
7
+ %>
8
+ <a href="<%= `/${_context.locale}/${_context.resourceConfig.articleTypePlural}/exports${queryString ? '?' + queryString : ''}` %>" class="btn-secondary">Export</a>
9
+ <% } %>
@@ -139,6 +139,42 @@
139
139
  <input type="text" name="f:keyword" value="<%= _context.query["f:keyword"] %>" />
140
140
  </div>
141
141
 
142
+ <div class="mb-4">
143
+ <label for="widget" class="font-semibold hover:cursor-pointer body-3">Widget</label>
144
+ <input type="text" name="f:widget" value="<%= _context.query["f:widget"] %>" />
145
+ </div>
146
+
147
+ <div class="mb-4">
148
+ <label for="widget_keyword" class="font-semibold hover:cursor-pointer body-3">Widget text keyword</label>
149
+ <input type="text" name="f:widget_keyword" value="<%= _context.query["f:widget_keyword"] %>" />
150
+ </div>
151
+
152
+ <div class="mb-4">
153
+ <label for="last_updated_from" class="font-semibold hover:cursor-pointer body-3">Last Updated from</label>
154
+ <input type="datetime-local" name="f:last_updated_from" value="<%= _context.query["f:last_updated_from"] %>" data-no-hints="true" />
155
+ </div>
156
+
157
+ <div class="mb-4">
158
+ <label for="last_updated_to" class="font-semibold hover:cursor-pointer body-3">Last Updated to</label>
159
+ <input type="datetime-local" name="f:last_updated_to" value="<%= _context.query["f:last_updated_to"] %>" data-no-hints="true" />
160
+ </div>
161
+
162
+ <div class="mb-4">
163
+ <label for="" class="font-semibold hover:cursor-pointer body-3">Translations</label>
164
+ <div class="checkbox-group mb-4">
165
+ <% translations.filter(item => item.code !== _context.locale).forEach((item,idx)=> { %>
166
+ <div class="flex items-center gap-1">
167
+ <% if (utils.includedInCollection(item.code, _context.query["f:translations"])) { %>
168
+ <input type="checkbox" name="f:translations" id='locale-<%= item.code %>' checked value="<%= item.code %>" class="hover:cursor-pointer">
169
+ <% } else { %>
170
+ <input type="checkbox" name="f:translations" id='locale-<%= item.code %>' value="<%= item.code %>" class="hover:cursor-pointer">
171
+ <% } %>
172
+ <label for='locale-<%= item.code %>' class="body-2 hover:cursor-pointer"><%= item.name %></label>
173
+ </div>
174
+ <% }); %>
175
+ </div>
176
+ </div>
177
+
142
178
  <div class="mb-4">
143
179
  <label for="" class="font-semibold hover:cursor-pointer body-3">Tags</label>
144
180
  <div class="checkbox-group mb-4">
@@ -157,8 +193,8 @@
157
193
 
158
194
  <div class="btn-toolbar">
159
195
  <button type="submit" class="btn-primary">Search</button>
196
+ <%- include('./_shared/_export_button.ejs') %>
160
197
  <a href="<%= `/${_context.locale}/${_context.resourceConfig.articleTypePlural}` %>" class="btn-text">Clear search</a>
161
198
  </div>
162
199
  </form>
163
200
  </div>
164
-
@@ -0,0 +1,9 @@
1
+ <% if (Object.keys(_context.query).some(k => k.startsWith('f:')) && items.list.length > 0) { %>
2
+ <%
3
+ const queryString = Object.entries(_context.query)
4
+ .filter(([key, value]) => value !== undefined && value !== null && value !== '')
5
+ .map(([key, value]) => Array.isArray(value) ? value.map(v => `${key}=${encodeURIComponent(v)}`).join('&') : `${key}=${encodeURIComponent(value)}`)
6
+ .join('&');
7
+ %>
8
+ <a href="<%= `/${_context.locale}/${_context.resourceConfig.eventTypePlural}/exports${queryString ? '?' + queryString : ''}` %>" class="btn-secondary">Export</a>
9
+ <% } %>