@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
@@ -1811,6 +1811,10 @@ select {
1811
1811
  width: 1.25rem;
1812
1812
  }
1813
1813
 
1814
+ .w-80 {
1815
+ width: 20rem;
1816
+ }
1817
+
1814
1818
  .w-9 {
1815
1819
  width: 2.25rem;
1816
1820
  }
@@ -1831,6 +1835,10 @@ select {
1831
1835
  width: 100%;
1832
1836
  }
1833
1837
 
1838
+ .flex-1 {
1839
+ flex: 1 1 0%;
1840
+ }
1841
+
1834
1842
  .flex-none {
1835
1843
  flex: none;
1836
1844
  }
@@ -2039,6 +2047,11 @@ select {
2039
2047
  padding: 1rem;
2040
2048
  }
2041
2049
 
2050
+ .px-3 {
2051
+ padding-left: 0.75rem;
2052
+ padding-right: 0.75rem;
2053
+ }
2054
+
2042
2055
  .px-4 {
2043
2056
  padding-left: 1rem;
2044
2057
  padding-right: 1rem;
@@ -2128,6 +2141,10 @@ select {
2128
2141
  font-weight: 600;
2129
2142
  }
2130
2143
 
2144
+ .uppercase {
2145
+ text-transform: uppercase;
2146
+ }
2147
+
2131
2148
  .lowercase {
2132
2149
  text-transform: lowercase;
2133
2150
  }
@@ -2218,6 +2235,12 @@ select {
2218
2235
  opacity: 0.6;
2219
2236
  }
2220
2237
 
2238
+ .shadow-inner {
2239
+ --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
2240
+ --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);
2241
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
2242
+ }
2243
+
2221
2244
  .shadow-lg {
2222
2245
  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
2223
2246
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -12,8 +12,8 @@
12
12
  "prepare": "npx tailwindcss -i ./src/styles/application.css -o ./dist/application.css"
13
13
  },
14
14
  "devDependencies": {
15
- "@babel/core": "7.28.6",
16
- "@babel/preset-env": "7.28.6",
15
+ "@babel/core": "7.29.0",
16
+ "@babel/preset-env": "7.29.0",
17
17
  "@babel/preset-typescript": "7.28.5",
18
18
  "@repo/typescript-config": "*",
19
19
  "@tailwindcss/forms": "0.5.7",
@@ -364,6 +364,7 @@ export default class HonoAdapter {
364
364
  if (result.headers) this.applyHeaders(result.headers, routerCtx);
365
365
  if (result.redirect) return routerCtx.redirect(result.redirect);
366
366
  if (result.json) return routerCtx.json(result.json);
367
+ if (result.text) return routerCtx.text(result.text);
367
368
 
368
369
  // Process global after filters
369
370
  for (const { pattern, filter } of this.globalAfterFilters) {
@@ -373,6 +374,7 @@ export default class HonoAdapter {
373
374
  if (afterResult.redirect)
374
375
  return routerCtx.redirect(afterResult.redirect);
375
376
  if (afterResult.json) return routerCtx.json(afterResult.json);
377
+ if (afterResult.text) return routerCtx.text(afterResult.text);
376
378
 
377
379
  Object.assign(
378
380
  assigns.afterFilterAssigns,
@@ -437,9 +439,11 @@ export default class HonoAdapter {
437
439
 
438
440
  formatQuery(queries) {
439
441
  const result = {};
440
- Object.entries(queries).forEach(([key, value]) => {
441
- result[key] = value.length === 1 ? value[0] : value;
442
- });
442
+ if (queries && typeof queries === "object") {
443
+ Object.entries(queries).forEach(([key, value]) => {
444
+ result[key] = value.length === 1 ? value[0] : value;
445
+ });
446
+ }
443
447
  return result;
444
448
  }
445
449
  }
@@ -1,4 +1,4 @@
1
- import { dateToUTCISO8601String, toDateInput } from "../utils";
1
+ import { dateToUTCISO8601String, toUTCISO8601String, toDateInput } from "../utils";
2
2
  import RaketaClient from "./RaketaClient";
3
3
  import {
4
4
  ArticleByIdDocument,
@@ -71,6 +71,13 @@ export default class Articles extends RaketaClient {
71
71
  async list(query, locale, articleTypeSlug) {
72
72
  const vars = this.buildListInput(query, locale);
73
73
 
74
+ if (vars.input.filters.last_updated_from) {
75
+ vars.input.filters.last_updated_from = toUTCISO8601String(vars.input.filters.last_updated_from);
76
+ }
77
+ if (vars.input.filters.last_updated_to) {
78
+ vars.input.filters.last_updated_to = toUTCISO8601String(vars.input.filters.last_updated_to);
79
+ }
80
+
74
81
  const { articles: items } = await this.query(ArticlesDocument, {
75
82
  input: {
76
83
  ...vars.input,
@@ -150,7 +157,7 @@ export default class Articles extends RaketaClient {
150
157
  paramsCopy.settings = JSON.stringify(paramsCopy.settings);
151
158
 
152
159
  const BatchTranslateDocument = buildBatchTranslateArticleDocument(
153
- locales.length
160
+ locales.length,
154
161
  );
155
162
 
156
163
  const variables = {};
@@ -204,7 +211,7 @@ export default class Articles extends RaketaClient {
204
211
  ArticleChangePreviewDocument,
205
212
  {
206
213
  input,
207
- }
214
+ },
208
215
  );
209
216
 
210
217
  return articleChangePreview;
@@ -86,6 +86,13 @@ export default class Events extends RaketaClient {
86
86
  async list(query, locale, eventTypeSlug) {
87
87
  const vars = this.buildListInput(query, locale);
88
88
 
89
+ if (vars.input.filters.last_updated_from) {
90
+ vars.input.filters.last_updated_from = toUTCISO8601String(vars.input.filters.last_updated_from);
91
+ }
92
+ if (vars.input.filters.last_updated_to) {
93
+ vars.input.filters.last_updated_to = toUTCISO8601String(vars.input.filters.last_updated_to);
94
+ }
95
+
89
96
  const { events: items } = await this.query(EventsDocument, {
90
97
  input: {
91
98
  ...vars.input,
@@ -165,7 +172,7 @@ export default class Events extends RaketaClient {
165
172
  paramsCopy.settings = JSON.stringify(paramsCopy.settings);
166
173
 
167
174
  const BatchTranslateDocument = buildBatchTranslateEventDocument(
168
- locales.length
175
+ locales.length,
169
176
  );
170
177
 
171
178
  const variables = {};
@@ -219,7 +226,7 @@ export default class Events extends RaketaClient {
219
226
  EventChangePreviewDocument,
220
227
  {
221
228
  input,
222
- }
229
+ },
223
230
  );
224
231
 
225
232
  return eventChangePreview;
@@ -1,3 +1,4 @@
1
+ import { toUTCISO8601String } from "../utils";
1
2
  import RaketaClient from "./RaketaClient";
2
3
  import {
3
4
  ChangePageSlugDocument,
@@ -61,6 +62,17 @@ export default class Pages extends RaketaClient {
61
62
  async list(query, locale, pageTypeSlug) {
62
63
  const vars = this.buildListInput(query, locale);
63
64
 
65
+ if (vars.input.filters.last_updated_from) {
66
+ vars.input.filters.last_updated_from = toUTCISO8601String(
67
+ vars.input.filters.last_updated_from,
68
+ );
69
+ }
70
+ if (vars.input.filters.last_updated_to) {
71
+ vars.input.filters.last_updated_to = toUTCISO8601String(
72
+ vars.input.filters.last_updated_to,
73
+ );
74
+ }
75
+
64
76
  const { pages: items } = await this.query(PagesDocument, {
65
77
  input: {
66
78
  ...vars.input,
@@ -141,7 +153,7 @@ export default class Pages extends RaketaClient {
141
153
  copyParams.settings = JSON.stringify(copyParams.settings);
142
154
 
143
155
  const BatchTranslateDocument = buildBatchTranslatePageDocument(
144
- locales.length
156
+ locales.length,
145
157
  );
146
158
 
147
159
  const variables = {};
@@ -18,6 +18,8 @@ function minimalArticleFragment() {
18
18
  title
19
19
  visibility
20
20
  publishAt
21
+ insertedAt
22
+ updatedAt
21
23
  }
22
24
  `;
23
25
  }
@@ -196,7 +198,7 @@ const buildBatchTranslateArticleDocument = (count) => gql`
196
198
  article ${articleFragment()}
197
199
  errors ${errorsFragment()}
198
200
  }
199
- `
201
+ `,
200
202
  )
201
203
  .join("\n")}
202
204
  }
@@ -240,7 +242,7 @@ const buildBatchStartArticleRevisionDocument = (count) => gql`
240
242
  errors ${errorsFragment()}
241
243
  articleErrors ${errorsFragment()}
242
244
  }
243
- `
245
+ `,
244
246
  )
245
247
  .join("\n")}
246
248
  }
@@ -20,6 +20,8 @@ function minimalEventFragment() {
20
20
  publishAt
21
21
  startsAt
22
22
  endsAt
23
+ insertedAt
24
+ updatedAt
23
25
  }
24
26
  `;
25
27
  }
@@ -204,7 +206,7 @@ const buildBatchTranslateEventDocument = (count) => gql`
204
206
  event ${eventFragment()}
205
207
  errors ${errorsFragment()}
206
208
  }
207
- `
209
+ `,
208
210
  )
209
211
  .join("\n")}
210
212
  }
@@ -232,7 +234,7 @@ const buildBatchStartEventRevisionDocument = (count) => gql`
232
234
  errors ${errorsFragment()}
233
235
  eventErrors ${errorsFragment()}
234
236
  }
235
- `
237
+ `,
236
238
  )
237
239
  .join("\n")}
238
240
  }
@@ -17,6 +17,8 @@ function minimalPageFragment() {
17
17
  slug
18
18
  title
19
19
  visibility
20
+ insertedAt
21
+ updatedAt
20
22
  }
21
23
  `;
22
24
  }
@@ -192,7 +194,7 @@ const buildBatchTranslatePageDocument = (count) => gql`
192
194
  page ${pageFragment()}
193
195
  errors ${errorsFragment()}
194
196
  }
195
- `
197
+ `,
196
198
  )
197
199
  .join("\n")}
198
200
  }
@@ -236,7 +238,7 @@ const buildBatchStartPageRevisionDocument = (count) => gql`
236
238
  errors ${errorsFragment()}
237
239
  pageErrors ${errorsFragment()}
238
240
  }
239
- `
241
+ `,
240
242
  )
241
243
  .join("\n")}
242
244
  }
@@ -14,6 +14,10 @@ const update = async (ctx) => {
14
14
  const current = await ctx.client.settings.find(ctx.locale);
15
15
  const updated = ctx.client.settings.deepMerge(current, params);
16
16
 
17
+ if (params._global_variables !== undefined) {
18
+ updated._global_variables = params._global_variables;
19
+ }
20
+
17
21
  const { errors } = await ctx.client.settings.update(ctx.locale, updated);
18
22
 
19
23
  if (errors) {
@@ -4,13 +4,19 @@ const index = async (ctx) => {
4
4
  const items = await ctx.client.articles.list(
5
5
  ctx.query,
6
6
  ctx.locale,
7
- ctx.resourceConfig.articleType
7
+ ctx.resourceConfig.articleType,
8
8
  );
9
9
  const tags = await ctx.client.tags.all(ctx.locale);
10
+ const languages = ctx.client.articles.getConfig().i18n.locales;
11
+ const translations = Object.keys(languages).map((key) => ({
12
+ name: languages[key].name,
13
+ code: key,
14
+ }));
10
15
 
11
16
  return {
12
17
  assigns: {
13
18
  tags: tags.list,
19
+ translations,
14
20
  items,
15
21
  visibilityOptions: ctx.client.articles.visibilityOptions(),
16
22
  },
@@ -36,7 +42,7 @@ const show = async (ctx) => {
36
42
  ],
37
43
  },
38
44
  id,
39
- ctx.locale
45
+ ctx.locale,
40
46
  );
41
47
 
42
48
  return {
@@ -81,13 +87,13 @@ const create = async (ctx) => {
81
87
 
82
88
  const resource = ctx.client.articles.deepMerge(
83
89
  ctx.client.articles.defaults(),
84
- ctx.client.articles.formatDates(params)
90
+ ctx.client.articles.formatDates(params),
85
91
  );
86
92
 
87
93
  const { errors } = await ctx.client.articles.create(
88
94
  resource,
89
95
  ctx.locale,
90
- ctx.resourceConfig.articleType
96
+ ctx.resourceConfig.articleType,
91
97
  );
92
98
 
93
99
  if (errors) {
@@ -0,0 +1,35 @@
1
+ import CSVExportHelper from "../../helpers/CSVExportHelper";
2
+
3
+ const exportCSV = async (ctx) => {
4
+ const { query, client, resourceConfig, remoteConfig } = ctx;
5
+ const articleType = resourceConfig.articleType;
6
+ const locale = query.locale || "en";
7
+
8
+ const articles = await client.articles.listAll(query, locale, articleType);
9
+
10
+ const fieldMapping = CSVExportHelper.standardFieldMapping(
11
+ remoteConfig,
12
+ locale,
13
+ articleType,
14
+ );
15
+
16
+ const headers = [...CSVExportHelper.standardHeaders];
17
+
18
+ const csv = CSVExportHelper.formatDataAsCSV(
19
+ articles.list,
20
+ headers,
21
+ fieldMapping,
22
+ );
23
+
24
+ return {
25
+ text: csv,
26
+ headers: {
27
+ "Content-Type": "text/csv",
28
+ "Content-Disposition": `attachment; filename="${CSVExportHelper.generateExportFilename(articleType)}"`,
29
+ },
30
+ };
31
+ };
32
+
33
+ export default {
34
+ index: exportCSV,
35
+ };
@@ -4,13 +4,19 @@ const index = async (ctx) => {
4
4
  const items = await ctx.client.events.list(
5
5
  ctx.query,
6
6
  ctx.locale,
7
- ctx.resourceConfig.eventType
7
+ ctx.resourceConfig.eventType,
8
8
  );
9
9
  const tags = await ctx.client.tags.all(ctx.locale);
10
+ const languages = ctx.client.events.getConfig().i18n.locales;
11
+ const translations = Object.keys(languages).map((key) => ({
12
+ name: languages[key].name,
13
+ code: key,
14
+ }));
10
15
 
11
16
  return {
12
17
  assigns: {
13
18
  tags: tags.list,
19
+ translations,
14
20
  items,
15
21
  visibilityOptions: ctx.client.events.visibilityOptions(),
16
22
  },
@@ -36,7 +42,7 @@ const show = async (ctx) => {
36
42
  ],
37
43
  },
38
44
  id,
39
- ctx.locale
45
+ ctx.locale,
40
46
  );
41
47
 
42
48
  return {
@@ -81,13 +87,13 @@ const create = async (ctx) => {
81
87
 
82
88
  const resource = ctx.client.events.deepMerge(
83
89
  ctx.client.events.defaults(),
84
- ctx.client.events.formatDates(params)
90
+ ctx.client.events.formatDates(params),
85
91
  );
86
92
 
87
93
  const { errors } = await ctx.client.events.create(
88
94
  resource,
89
95
  ctx.locale,
90
- ctx.resourceConfig.eventType
96
+ ctx.resourceConfig.eventType,
91
97
  );
92
98
 
93
99
  if (errors) {
@@ -0,0 +1,35 @@
1
+ import CSVExportHelper from "../../helpers/CSVExportHelper";
2
+
3
+ const exportCSV = async (ctx) => {
4
+ const { query, client, resourceConfig, remoteConfig } = ctx;
5
+ const eventType = resourceConfig.eventType;
6
+ const locale = query.locale || "en";
7
+
8
+ const events = await client.events.listAll(query, locale, eventType);
9
+
10
+ const fieldMapping = CSVExportHelper.standardFieldMapping(
11
+ remoteConfig,
12
+ locale,
13
+ eventType,
14
+ );
15
+
16
+ const headers = [...CSVExportHelper.standardHeaders];
17
+
18
+ const csv = CSVExportHelper.formatDataAsCSV(
19
+ events.list,
20
+ headers,
21
+ fieldMapping,
22
+ );
23
+
24
+ return {
25
+ text: csv,
26
+ headers: {
27
+ "Content-Type": "text/csv",
28
+ "Content-Disposition": `attachment; filename="${CSVExportHelper.generateExportFilename(eventType)}"`,
29
+ },
30
+ };
31
+ };
32
+
33
+ export default {
34
+ index: exportCSV,
35
+ };
@@ -0,0 +1,35 @@
1
+ import CSVExportHelper from "../../helpers/CSVExportHelper";
2
+
3
+ const exportCSV = async (ctx) => {
4
+ const { query, client, resourceConfig, remoteConfig } = ctx;
5
+ const pageType = resourceConfig.pageType;
6
+ const locale = query.locale || "en";
7
+
8
+ const pages = await client.pages.listAll(query, locale, pageType);
9
+
10
+ const fieldMapping = CSVExportHelper.standardFieldMapping(
11
+ remoteConfig,
12
+ locale,
13
+ pageType,
14
+ );
15
+
16
+ const headers = [...CSVExportHelper.standardHeaders];
17
+
18
+ const csv = CSVExportHelper.formatDataAsCSV(
19
+ pages.list,
20
+ headers,
21
+ fieldMapping,
22
+ );
23
+
24
+ return {
25
+ text: csv,
26
+ headers: {
27
+ "Content-Type": "text/csv",
28
+ "Content-Disposition": `attachment; filename="${CSVExportHelper.generateExportFilename(pageType)}"`,
29
+ },
30
+ };
31
+ };
32
+
33
+ export default {
34
+ index: exportCSV,
35
+ };
@@ -2,14 +2,20 @@ const index = async (ctx) => {
2
2
  const items = await ctx.client.pages.list(
3
3
  ctx.query,
4
4
  ctx.locale,
5
- ctx.resourceConfig.pageType
5
+ ctx.resourceConfig.pageType,
6
6
  );
7
7
 
8
8
  const tags = await ctx.client.tags.all(ctx.locale);
9
+ const languages = ctx.client.pages.getConfig().i18n.locales;
10
+ const translations = Object.keys(languages).map((key) => ({
11
+ name: languages[key].name,
12
+ code: key,
13
+ }));
9
14
 
10
15
  return {
11
16
  assigns: {
12
17
  tags: tags.list,
18
+ translations,
13
19
  items,
14
20
  visibilityOptions: ctx.client.pages.visibilityOptions(),
15
21
  },
@@ -35,7 +41,7 @@ const show = async (ctx) => {
35
41
  ],
36
42
  },
37
43
  id,
38
- ctx.locale
44
+ ctx.locale,
39
45
  );
40
46
 
41
47
  return {
@@ -79,13 +85,13 @@ const create = async (ctx) => {
79
85
  const params = { ...ctx.resourceParams.resource };
80
86
  const resource = ctx.client.pages.deepMerge(
81
87
  ctx.client.pages.defaults(),
82
- params
88
+ params,
83
89
  );
84
90
 
85
91
  const { errors } = await ctx.client.pages.create(
86
92
  resource,
87
93
  ctx.locale,
88
- ctx.resourceConfig.pageType
94
+ ctx.resourceConfig.pageType,
89
95
  );
90
96
 
91
97
  if (errors) {
@@ -0,0 +1,103 @@
1
+ function escapeCSVValue(value) {
2
+ if (value === null || value === undefined) {
3
+ return "";
4
+ }
5
+
6
+ const stringValue = String(value);
7
+
8
+ if (
9
+ stringValue.includes(",") ||
10
+ stringValue.includes('"') ||
11
+ stringValue.includes("\n") ||
12
+ stringValue.includes("\r")
13
+ ) {
14
+ return `"${stringValue.replace(/"/g, '""')}"`;
15
+ }
16
+
17
+ return stringValue;
18
+ }
19
+
20
+ function formatDataAsCSV(data, headers, fieldMapping) {
21
+ const headerRow = headers.join(",");
22
+
23
+ const rows = data.map((item) => {
24
+ return headers
25
+ .map((header) => {
26
+ const field = fieldMapping[header];
27
+ let value = "";
28
+
29
+ if (typeof field === "function") {
30
+ value = field(item);
31
+ } else if (field.includes(".")) {
32
+ const props = field.split(".");
33
+ let nestedValue = item;
34
+
35
+ for (const prop of props) {
36
+ if (nestedValue && nestedValue[prop] !== undefined) {
37
+ nestedValue = nestedValue[prop];
38
+ } else {
39
+ nestedValue = "";
40
+ break;
41
+ }
42
+ }
43
+
44
+ value = nestedValue;
45
+ } else {
46
+ value = item[field] !== undefined ? item[field] : "";
47
+ }
48
+
49
+ return escapeCSVValue(value);
50
+ })
51
+ .join(",");
52
+ });
53
+
54
+ return [headerRow, ...rows].join("\r\n");
55
+ }
56
+
57
+ function generateExportFilename(contentType) {
58
+ const date = new Date();
59
+ const day = String(date.getDate()).padStart(2, "0");
60
+ const month = String(date.getMonth() + 1).padStart(2, "0");
61
+ const year = String(date.getFullYear()).slice(2);
62
+
63
+ return `${day}${month}${year}_${contentType}_export.csv`;
64
+ }
65
+
66
+ const extractResourceType = (resource, type) => {
67
+ if (resource.pageType) return resource.pageType;
68
+ if (resource.articleType) return resource.articleType;
69
+ if (resource.eventType) return resource.eventType;
70
+ if (resource.resourceType) return resource.resourceType;
71
+ if (resource.type) return resource.type;
72
+ return type;
73
+ };
74
+
75
+ const standardFieldMapping = (remoteConfig, locale, type) => ({
76
+ id: "id",
77
+ type: (item) => extractResourceType(item, type),
78
+ locale: (item) => (item.locale ? item.locale : locale),
79
+ title: "title",
80
+ URL: (item) => remoteConfig.resourceUrl(item, type, locale),
81
+ visibility: "visibility",
82
+ created_at: "insertedAt",
83
+ updated_at: "updatedAt",
84
+ });
85
+
86
+ const standardHeaders = [
87
+ "id",
88
+ "type",
89
+ "locale",
90
+ "title",
91
+ "URL",
92
+ "visibility",
93
+ "created_at",
94
+ "updated_at",
95
+ ];
96
+
97
+ export default {
98
+ escapeCSVValue,
99
+ formatDataAsCSV,
100
+ generateExportFilename,
101
+ standardFieldMapping,
102
+ standardHeaders,
103
+ };
@@ -15,6 +15,7 @@ import ArticlesHandler from "./handlers/articles/ArticlesHandler";
15
15
  import ChangeHandler from "./handlers/articles/ChangeHandler";
16
16
  import ClonesHandler from "./handlers/articles/ClonesHandler";
17
17
  import DeletesHandler from "./handlers/articles/DeletesHandler";
18
+ import ExportsHandler from "./handlers/articles/ExportsHandler";
18
19
  import HistoryHandler from "./handlers/articles/HistoryHandler";
19
20
  import TranslationsHandler from "./handlers/articles/TranslationsHandler";
20
21
  import URLsHandler from "./handlers/articles/URLsHandler";
@@ -35,6 +36,18 @@ function routeArticlesModule(
35
36
  articleTypePlural,
36
37
  };
37
38
 
39
+ const exportsMapping = [
40
+ [
41
+ "GET",
42
+ `/${baseResource}/exports`,
43
+ "exports",
44
+ "articles/exports",
45
+ "index",
46
+ ExportsHandler.index,
47
+ ],
48
+ ];
49
+ adapter.routeCollection(exportsMapping, resourceConfig);
50
+
38
51
  routeCollection(adapter, baseResource, ArticlesHandler, {
39
52
  ...resourceConfig,
40
53
  viewsFallback: "articles",