@raketa-cloud/admin 1.2.0 → 1.4.0

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/HonoAdapter.js +304 -169
  3. package/src/filters.js +17 -0
  4. package/src/handlers/article_revisions/ArticleRevisionsHandler.js +8 -0
  5. package/src/handlers/articles/ArticlesHandler.js +12 -1
  6. package/src/handlers/event_revisions/EventRevisionsHandler.js +8 -0
  7. package/src/handlers/events/EventsHandler.js +13 -2
  8. package/src/handlers/page_revisions/PageRevisionsHandler.js +8 -0
  9. package/src/handlers/pages/PagesHandler.js +12 -1
  10. package/src/index.js +1 -0
  11. package/src/routeCollection.js +14 -83
  12. package/src/routeMember.js +10 -33
  13. package/src/routeNestedCollection.js +19 -47
  14. package/src/routeNestedMember.js +14 -40
  15. package/src/tests/Adapter.test.js +4 -4
  16. package/src/tests/HonoAdapter.filter.test.js +40 -0
  17. package/src/tests/HonoAdapter.test.js +110 -9
  18. package/src/tests/RaketaContext.test.js +65 -0
  19. package/src/tests/cipher.test.js +73 -0
  20. package/src/tests/filters.test.js +97 -0
  21. package/src/tests/handlers/article_revisions/ArticleRevisionsHandler.test/create.test.js +43 -35
  22. package/src/tests/handlers/articles/ArticlesHandler.test/show.test.js +12 -1
  23. package/src/tests/handlers/event_revisions/EventRevisionsHandler.test/create.test.js +41 -33
  24. package/src/tests/handlers/events/EventsHandler.test/show.test.js +12 -1
  25. package/src/tests/handlers/page_revisions/PageRevisionsHandler.test/create.test.js +41 -27
  26. package/src/tests/handlers/pages/PagesHandler.test/show.test.js +12 -1
  27. package/src/tests/routeCollection.test.js +0 -92
  28. package/src/tests/routeMember.test.js +0 -6
  29. package/src/tests/routeNestedCollection.test.js +0 -14
  30. package/src/tests/routeNestedMember.test.js +0 -6
  31. package/src/tests/utils.test.js +70 -0
  32. package/src/views/articles/_shared/_history_toolbar.ejs +2 -2
  33. package/src/views/articles/_shared/_toolbar.ejs +38 -26
  34. package/src/views/articles/_shared/history_table.ejs +6 -0
  35. package/src/views/events/_shared/_history_toolbar.ejs +2 -2
  36. package/src/views/events/_shared/_toolbar.ejs +38 -26
  37. package/src/views/events/_shared/history_table.ejs +6 -0
  38. package/src/views/pages/_shared/_history_toolbar.ejs +2 -2
  39. package/src/views/pages/_shared/_toolbar.ejs +38 -26
  40. package/src/views/pages/_shared/history_table.ejs +6 -0
  41. package/src/Adapter.js +0 -265
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -1,54 +1,241 @@
1
- import Adapter from "./Adapter";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ejs from "ejs";
2
4
  import Logger from "./Logger";
5
+ import currentPath from "./currentPath.cjs";
6
+ import FormBuilder from "./helpers/FormBuilder";
7
+ import HTMLBuilder from "./helpers/HTMLBuilder";
8
+ import Inflector from "./helpers/Inflector";
3
9
  import RaketaContext from "./RaketaContext";
4
10
  import setupAuthentication from "./setupAuthentication";
11
+ import form_for from "./helpers/form_for";
12
+ import history from "./helpers/historyUtils";
13
+ import pagination from "./helpers/pagination_helper";
14
+ import sort_button from "./helpers/sort_button";
15
+ import utils from "./helpers/utils";
16
+
17
+ function escapeHtml(string) {
18
+ const escape = {
19
+ '"': '"',
20
+ '&': '&',
21
+ "'": ''',
22
+ '<': '&lt;',
23
+ '>': '&gt;'
24
+ };
25
+
26
+ return String(string).replace(/["&'<>]/g, char => escape[char]);
27
+ }
28
+
29
+ const renderError = (message, stack) => {
30
+ const errorCSS = `
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ html { font-family: sans-serif; font-size: 16px; padding: 2rem; background-color: pink; }
33
+ h1 { margin-bottom: 1rem; color: FireBrick; }
34
+ .fail-squid { font-size: 7rem; }
35
+ pre { padding: 1.5rem 2rem; border-radius: 0.5rem; background-color: black; color: white; line-height: 1.5; font-size: 1.25rem; }
36
+ `;
37
+ return `<style>${errorCSS}</style><div class="fail-squid">🦑</div><h1>${message}</h1><pre>${escapeHtml(stack)}</pre>`;
38
+ };
39
+
40
+ // Convert to array if needed
41
+ const toArray = value => Array.isArray(value) ? value : [value].filter(Boolean);
42
+
43
+ // Handle nested form values
44
+ function putIn(result, key, value) {
45
+ const segments = key.split(/[\[\]]+/).filter(Boolean);
46
+ let current = result;
47
+
48
+ segments.forEach((segment, idx) => {
49
+ const isLastSegment = idx === segments.length - 1;
50
+
51
+ if (isLastSegment) {
52
+ // Set value at final segment
53
+ current[segment] = key.endsWith("[]") || Array.isArray(value)
54
+ ? toArray(value)
55
+ : value;
56
+ } else {
57
+ // Create path to next segment
58
+ current[segment] ??= {};
59
+ current = current[segment];
60
+ }
61
+ });
62
+ }
63
+
64
+ // Normalize paths for consistent handling
65
+ const sanitizePath = filePath => path.normalize(path.join("/", ...filePath.split("/")));
66
+
67
+ // Find template path with fallbacks
68
+ function getPartialPath(partialPath, views, viewsFallback) {
69
+ const paths = [
70
+ sanitizePath(`${process.cwd()}/src/views/${views}/${partialPath}`),
71
+ sanitizePath(`${currentPath}/views/${views}/${partialPath}`),
72
+ viewsFallback && sanitizePath(`${currentPath}/views/${viewsFallback}/${partialPath}`)
73
+ ].filter(Boolean);
74
+
75
+ // Return first existing path
76
+ const existingPath = paths.find(path => existsSync(path));
77
+
78
+ if (existingPath) return existingPath;
79
+
80
+ throw new Error(`Template not found: "${partialPath}"\nSearched in:\n${paths.join('\n')}`);
81
+ }
5
82
 
83
+ // Parse request body based on content type
6
84
  async function parseBody(req) {
7
- switch (req.header("Content-type")) {
8
- case "multipart/form-data":
9
- case "application/x-www-form-urlencoded":
10
- return req.parseBody();
11
- case "application/json":
12
- return req.json();
13
- case "text/plain":
14
- return req.text();
15
- default:
16
- // That is an ok default for now
17
- return req.parseBody();
18
- }
85
+ const contentType = req.header("Content-type") || "";
86
+
87
+ if (contentType.includes("json")) return req.json();
88
+ if (contentType === "text/plain") return req.text();
89
+
90
+ // Default for form data and other content types
91
+ return req.parseBody();
19
92
  }
20
93
 
94
+ // Convert body to request parameters
21
95
  function bodyToParams(req, parsedBody, adapter) {
22
- switch (req.header("Content-type")) {
23
- case "multipart/form-data":
24
- case "application/x-www-form-urlencoded":
25
- return adapter.parseFormBody(parsedBody);
26
- default:
27
- return parsedBody;
96
+ const contentType = req.header("Content-type") || "";
97
+
98
+ if (contentType.includes("form-data") || contentType.includes("urlencoded")) {
99
+ return adapter.parseFormBody(parsedBody);
28
100
  }
101
+
102
+ return parsedBody;
29
103
  }
30
104
 
31
- export default class HonoAdapter extends Adapter {
105
+ export default class HonoAdapter {
32
106
  constructor(router, config) {
33
- super(router);
34
107
  this.globalCtx = (ctx) => ctx;
35
108
  this.router = router;
36
109
  this.globalConfig = config;
110
+ this.globalBeforeFilters = [];
111
+ this.globalAfterFilters = [];
37
112
 
38
113
  // TODO: Pass in a path to redirect to after login?
39
- setupAuthentication(
40
- router,
41
- config
42
- );
43
-
44
- router.get("/", (c) => {
45
- return c.redirect("/en");
46
- });
114
+ if (config) {
115
+ setupAuthentication(router, config);
116
+
117
+ router.get("/", (c) => {
118
+ return c.redirect("/en");
119
+ });
120
+ }
47
121
  }
48
122
 
49
123
  setGlobalContext(globalCtx) {
50
124
  this.globalCtx = globalCtx;
51
125
  }
126
+
127
+ setBeforeFilter(pattern, filter) {
128
+ this.globalBeforeFilters.push({ pattern, filter });
129
+ }
130
+
131
+ setAfterFilter(pattern, filter) {
132
+ this.globalAfterFilters.push({ pattern, filter });
133
+ }
134
+
135
+ getActionPath(views, viewsFallback, action) {
136
+ const packagePath = viewsFallback
137
+ ? `${currentPath}/views/${viewsFallback}/${action}.ejs`
138
+ : `${currentPath}/views/${views}/${action}.ejs`;
139
+
140
+ const appPath = `${process.cwd()}/src/views/${views}/${action}.ejs`;
141
+
142
+ return existsSync(appPath) ? appPath : packagePath;
143
+ }
144
+
145
+ getHelpers(viewHelpersConfig) {
146
+ const t = new HTMLBuilder();
147
+ const f = new FormBuilder();
148
+
149
+ return {
150
+ t,
151
+ f,
152
+ form_for: form_for(viewHelpersConfig),
153
+ history,
154
+ utils,
155
+ sort_button,
156
+ pagination,
157
+ Inflector,
158
+ };
159
+ }
160
+
161
+ async renderLayout(assigns, body) {
162
+ const layoutTemplate = `${currentPath}/views/layouts/application.ejs`;
163
+
164
+ return await ejs.renderFile(layoutTemplate, {
165
+ ...assigns,
166
+ body,
167
+ });
168
+ }
169
+
170
+ renderPartial(assigns, views, viewsFallback) {
171
+ return function render(relativePath, localAssigns) {
172
+ const partialPath = getPartialPath(relativePath, views, viewsFallback);
173
+
174
+ try {
175
+ // Synchronously read the partial file as ejs does not support async/await
176
+ const partialTemplate = readFileSync(partialPath, "utf8");
177
+ const rendered = ejs.render(
178
+ partialTemplate,
179
+ {
180
+ ...assigns,
181
+ ...localAssigns,
182
+ },
183
+ { filename: partialPath }
184
+ );
185
+
186
+ return rendered;
187
+ } catch (e) {
188
+ const error = `Error rendering:\n ${partialPath}`;
189
+ Logger.error(`${error}\n\n${e.stack}`);
190
+
191
+ return renderError(error, e.stack);
192
+ }
193
+ };
194
+ }
195
+
196
+ async renderAction(
197
+ { views, viewsFallback, action, assigns },
198
+ viewHelpersConfig = {}
199
+ ) {
200
+ // Pick the parent here in the views so we can reuse stuff...
201
+ const fullTemplatePath = this.getActionPath(views, viewsFallback, action);
202
+ const renderAssigns = { ...this.getHelpers(viewHelpersConfig), ...assigns };
203
+ const renderPartialHelper = this.renderPartial(
204
+ renderAssigns,
205
+ views,
206
+ viewsFallback
207
+ );
208
+
209
+ try {
210
+ // Synchronously read the full template file
211
+ const template = readFileSync(fullTemplatePath, "utf8");
212
+
213
+ // Render the full template with synchronous partial rendering
214
+ const rendered = ejs.render(
215
+ template,
216
+ {
217
+ ...renderAssigns,
218
+ render_partial: renderPartialHelper,
219
+ },
220
+ { filename: fullTemplatePath }
221
+ );
222
+
223
+ const fullPage = this.renderLayout(renderAssigns, rendered);
224
+
225
+ return fullPage;
226
+ } catch (e) {
227
+ const error = `Error rendering:\n ${fullTemplatePath}`;
228
+ Logger.error(`${error}\n\n${e.stack}`);
229
+
230
+ return renderError(error, e.stack);
231
+ }
232
+ }
233
+
234
+ parseFormBody(body) {
235
+ const result = {};
236
+ Object.entries(body).forEach(([key, value]) => putIn(result, key, value));
237
+ return result;
238
+ }
52
239
 
53
240
  async createContext(router, next, verb, resource, action) {
54
241
  const body = await parseBody(router.req);
@@ -74,196 +261,144 @@ export default class HonoAdapter extends Adapter {
74
261
  });
75
262
  }
76
263
 
77
- async createAction(
78
- [verb, path, resource, views, action, handler, before_filter, after_filter],
79
- resourceConfig
80
- ) {
264
+ async createAction([verb, path, resource, views, action, handler], resourceConfig) {
81
265
  const method = this.getMethod(verb);
82
-
83
- Object.keys(this.globalConfig.i18n.locales).forEach((currentLocale) => {
266
+ const { i18n } = this.globalConfig;
267
+
268
+ Object.keys(i18n.locales).forEach((currentLocale) => {
84
269
  const completePath = `/${currentLocale}${path}`;
85
-
86
- // A terrible, terrible hack...
87
- // So how do we override for the entire handler.. do we need to completely override it, or just partially? Fun stuff?
88
- this.router.router.routes = this.router.router.routes.filter((route) => {
89
- return route[0] !== verb || route[1] !== completePath;
90
- });
270
+
271
+ // Clean up existing routes with same path/verb
272
+ this.router.router.routes = this.router.router.routes.filter(route =>
273
+ route[0] !== verb || route[1] !== completePath
274
+ );
91
275
 
92
276
  method(completePath, async (routerCtx, next) => {
93
- const ctx = await this.createContext(
94
- routerCtx,
95
- next,
96
- verb,
97
- resource,
98
- action
99
- );
100
-
101
- ctx.resourceConfig = resourceConfig;
102
- ctx.locales = this.globalConfig.i18n.locales;
103
- ctx.defaultLocale = this.globalConfig.i18n.defaultLocale;
104
- ctx.locale = currentLocale;
105
- ctx.remoteConfig = { ...this.globalConfig.remoteConfig };
106
- ctx.logger = Logger;
277
+ // Create context with request data
278
+ const ctx = await this.createContext(routerCtx, next, verb, resource, action);
279
+
280
+ // Set context properties
281
+ Object.assign(ctx, {
282
+ resourceConfig,
283
+ locales: i18n.locales,
284
+ defaultLocale: i18n.defaultLocale,
285
+ maxOpenRevisions: this.globalConfig?.maxOpenRevisions || 3,
286
+ locale: currentLocale,
287
+ remoteConfig: { ...this.globalConfig.remoteConfig },
288
+ logger: Logger
289
+ });
107
290
 
291
+ // Build context object
108
292
  const _context = {
109
- timezones: this.globalConfig.i18n.timezones,
293
+ timezones: i18n.timezones,
110
294
  locales: ctx.locales,
111
295
  locale: ctx.locale,
112
- defaultLocale: ctx.defaultLocale,
296
+ defaultLocale: ctx.defaultLocale,
297
+ maxOpenRevisions: ctx.maxOpenRevisions,
113
298
  path: ctx.req.path,
114
299
  query: ctx.query || {},
115
- layout: {
116
- ...this.globalConfig.layout,
117
- },
118
- resourceConfig: resourceConfig,
119
- adminJwt: ctx.authentication.token,
300
+ layout: { ...this.globalConfig.layout },
301
+ resourceConfig,
302
+ adminJwt: ctx.authentication?.token,
120
303
  remoteConfig: { ...this.globalConfig.remoteConfig },
121
- globalConfig: this.globalConfig,
304
+ globalConfig: this.globalConfig
122
305
  };
123
306
 
124
- ctx.logger.info(
125
- `${ctx.method} "${ctx.rawPath}" -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`
126
- );
127
-
307
+ ctx.logger.info(`${ctx.method} "${ctx.rawPath}" -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`);
308
+
128
309
  const assigns = {
129
310
  beforeFilterAssigns: {},
130
311
  actionAssigns: {},
131
- afterFilterAssigns: {},
312
+ afterFilterAssigns: {}
132
313
  };
133
314
 
134
315
  // Initialize global context
135
316
  ctx.initGlobalContext();
136
317
 
137
- // Process before filters
138
- if (before_filter) {
139
- const {
140
- redirect,
141
- json,
142
- assigns: beforeFilterAssigns = {},
143
- } = before_filter(ctx) || {};
144
-
145
- if (redirect) return routerCtx.redirect(redirect);
146
-
147
- if (json) {
148
- return routerCtx.json(json);
318
+ // Process global before filters
319
+ for (const { pattern, filter } of this.globalBeforeFilters) {
320
+ if (pattern.test(ctx.path)) {
321
+ const result = filter(ctx) || {};
322
+
323
+ if (result.redirect) return routerCtx.redirect(result.redirect);
324
+ if (result.json) return routerCtx.json(result.json);
325
+
326
+ Object.assign(assigns.beforeFilterAssigns, result.assigns || {});
149
327
  }
150
-
151
- assigns.beforeFilterAssigns = beforeFilterAssigns;
152
328
  }
153
329
 
154
- // Process action
155
- const {
156
- headers,
157
- redirect,
158
- json,
159
- assigns: actionAssigns = {},
160
- } = (await handler(ctx, resourceConfig)) || {};
161
- assigns.actionAssigns = actionAssigns;
162
-
163
- this.applyHeaders(headers, routerCtx);
164
-
165
- if (redirect) return routerCtx.redirect(redirect);
166
-
167
- // Process after filters
168
- if (after_filter) {
169
- const {
170
- redirect,
171
- json,
172
- assigns: afterFilterAssigns = {},
173
- } = after_filter(ctx) || {};
174
-
175
- if (redirect) return routerCtx.redirect(redirect);
176
-
177
- if (json) {
178
- return routerCtx.json(json);
330
+ // Process action handler
331
+ const result = await handler(ctx, resourceConfig) || {};
332
+ assigns.actionAssigns = result.assigns || {};
333
+
334
+ if (result.headers) this.applyHeaders(result.headers, routerCtx);
335
+ if (result.redirect) return routerCtx.redirect(result.redirect);
336
+ if (result.json) return routerCtx.json(result.json);
337
+
338
+ // Process global after filters
339
+ for (const { pattern, filter } of this.globalAfterFilters) {
340
+ if (pattern.test(ctx.path)) {
341
+ const afterResult = filter(ctx) || {};
342
+
343
+ if (afterResult.redirect) return routerCtx.redirect(afterResult.redirect);
344
+ if (afterResult.json) return routerCtx.json(afterResult.json);
345
+
346
+ Object.assign(assigns.afterFilterAssigns, afterResult.assigns || {});
179
347
  }
180
-
181
- assigns.afterFilterAssigns = afterFilterAssigns;
182
348
  }
183
349
 
350
+ // Merge all assigns
184
351
  const fullAssigns = {
185
352
  ...ctx.assigns,
186
353
  ...assigns.beforeFilterAssigns,
187
354
  ...assigns.actionAssigns,
188
355
  ...assigns.afterFilterAssigns,
189
- _context: _context,
356
+ _context
190
357
  };
191
358
 
192
- if (json) {
193
- return routerCtx.json(json);
194
- }
195
-
196
- const rendered = this.renderAction(
197
- {
198
- views: views,
199
- viewsFallback: resourceConfig.viewsFallback,
200
- action,
201
- assigns: fullAssigns,
202
- },
203
- this.globalConfig?.helpers || {}
204
- );
359
+ // Render the view
360
+ const rendered = this.renderAction({
361
+ views,
362
+ viewsFallback: resourceConfig.viewsFallback,
363
+ action,
364
+ assigns: fullAssigns
365
+ }, this.globalConfig?.helpers || {});
205
366
 
206
367
  return routerCtx.html(rendered);
207
368
  });
208
369
  });
209
370
  }
210
371
 
372
+ // Single method to handle all route types
211
373
  routeCollection(mapping, resourceConfig) {
212
- mapping.forEach((handlerMap) =>
213
- this.createAction(handlerMap, resourceConfig)
214
- );
374
+ mapping.forEach(handlerMap => this.createAction(handlerMap, resourceConfig));
215
375
  }
216
376
 
217
- routeNestedCollection(mapping, resourceConfig) {
218
- mapping.forEach((handlerMap) =>
219
- this.createAction(handlerMap, resourceConfig)
220
- );
221
- }
222
-
223
- routeMember(mapping, resourceConfig) {
224
- mapping.forEach((handlerMap) =>
225
- this.createAction(handlerMap, resourceConfig)
226
- );
227
- }
228
-
229
- routeNestedMember(mapping, resourceConfig) {
230
- mapping.forEach((handlerMap) =>
231
- this.createAction(handlerMap, resourceConfig)
232
- );
233
- }
377
+ // Use the same implementation for all route types
378
+ routeNestedCollection = this.routeCollection;
379
+ routeMember = this.routeCollection;
380
+ routeNestedMember = this.routeCollection;
234
381
 
235
382
  getMethod(verb) {
236
- return (
237
- {
238
- GET: this.router.get,
239
- POST: this.router.post,
240
- PUT: this.router.put,
241
- DELETE: this.router.delete,
242
- }[verb] || this.router.get
243
- );
383
+ const methods = {
384
+ GET: this.router.get,
385
+ POST: this.router.post,
386
+ PUT: this.router.put,
387
+ DELETE: this.router.delete,
388
+ };
389
+ return methods[verb] || this.router.get;
244
390
  }
245
391
 
246
392
  applyHeaders(headers, router) {
247
- if (!headers) {
248
- return;
249
- }
250
-
251
- for (const [key, value] of Object.entries(headers)) {
252
- router.header(key, value);
253
- }
393
+ if (!headers) return;
394
+ Object.entries(headers).forEach(([key, value]) => router.header(key, value));
254
395
  }
255
396
 
256
397
  formatQuery(queries) {
257
398
  const result = {};
258
-
259
- for (const [key, value] of Object.entries(queries)) {
260
- if (value.length === 1) {
261
- result[key] = value[0];
262
- } else {
263
- result[key] = value;
264
- }
265
- }
266
-
267
- return { ...result };
399
+ Object.entries(queries).forEach(([key, value]) => {
400
+ result[key] = value.length === 1 ? value[0] : value;
401
+ });
402
+ return result;
268
403
  }
269
- }
404
+ }
package/src/filters.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Sets a global filter to be applied to routes matching the pattern
3
+ * @param {HonoAdapter} adapter - The adapter instance
4
+ * @param {RegExp} pattern - Route pattern to match
5
+ * @param {Function} filter - The filter function
6
+ */
7
+ export const setBeforeFilter = (adapter, pattern, filter) =>
8
+ adapter.setBeforeFilter(pattern, filter);
9
+
10
+ /**
11
+ * Sets a global after filter to be applied to routes matching the pattern
12
+ * @param {HonoAdapter} adapter - The adapter instance
13
+ * @param {RegExp} pattern - Route pattern to match
14
+ * @param {Function} filter - The filter function
15
+ */
16
+ export const setAfterFilter = (adapter, pattern, filter) =>
17
+ adapter.setAfterFilter(pattern, filter);
@@ -1,6 +1,14 @@
1
1
  const create = async (ctx) => {
2
2
  const { parent_id } = ctx.params;
3
3
 
4
+ const article = await ctx.client.articles.find(parent_id, ctx.locale);
5
+
6
+ if (article.revisionsCount >= ctx.maxOpenRevisions) {
7
+ return {
8
+ redirect: `/${ctx.locale}/${ctx.resourceConfig.articleTypePlural}/${parent_id}?revisions_count_error=1`,
9
+ };
10
+ }
11
+
4
12
  const { errors, articleErrors } = await ctx.client.articleRevisions.create(
5
13
  parent_id,
6
14
  ctx.locale
@@ -26,7 +26,18 @@ const show = async (ctx) => {
26
26
  }
27
27
 
28
28
  const revisions = await ctx.client.articleRevisions.all(id, ctx.locale);
29
- const changes = await ctx.client.articleChanges.list({}, id, ctx.locale);
29
+ const changes = await ctx.client.articleChanges.list(
30
+ {
31
+ "f:change_type": [
32
+ "revision_publish",
33
+ "revision_lock",
34
+ "revision_unlock",
35
+ "update",
36
+ ],
37
+ },
38
+ id,
39
+ ctx.locale
40
+ );
30
41
 
31
42
  return {
32
43
  assigns: {
@@ -1,6 +1,14 @@
1
1
  const create = async (ctx) => {
2
2
  const { parent_id } = ctx.params;
3
3
 
4
+ const event = await ctx.client.events.find(parent_id, ctx.locale);
5
+
6
+ if (event.revisionsCount >= ctx.maxOpenRevisions) {
7
+ return {
8
+ redirect: `/${ctx.locale}/${ctx.resourceConfig.eventTypePlural}/${parent_id}?revisions_count_error=1`,
9
+ };
10
+ }
11
+
4
12
  const { errors, eventErrors } = await ctx.client.eventRevisions.create(
5
13
  parent_id,
6
14
  ctx.locale
@@ -1,4 +1,4 @@
1
- import { toDateTimeInput, toDateInput } from "../../utils";
1
+ import { toDateInput, toDateTimeInput } from "../../utils";
2
2
 
3
3
  const index = async (ctx) => {
4
4
  const items = await ctx.client.events.list(
@@ -26,7 +26,18 @@ const show = async (ctx) => {
26
26
  }
27
27
 
28
28
  const revisions = await ctx.client.eventRevisions.all(id, ctx.locale);
29
- const changes = await ctx.client.eventChanges.list({}, id, ctx.locale);
29
+ const changes = await ctx.client.eventChanges.list(
30
+ {
31
+ "f:change_type": [
32
+ "revision_publish",
33
+ "revision_lock",
34
+ "revision_unlock",
35
+ "update",
36
+ ],
37
+ },
38
+ id,
39
+ ctx.locale
40
+ );
30
41
 
31
42
  return {
32
43
  assigns: {
@@ -1,6 +1,14 @@
1
1
  const create = async (ctx) => {
2
2
  const { parent_id } = ctx.params;
3
3
 
4
+ const page = await ctx.client.pages.find(parent_id, ctx.locale);
5
+
6
+ if (page.revisionsCount >= ctx.maxOpenRevisions) {
7
+ return {
8
+ redirect: `/${ctx.locale}/${ctx.resourceConfig.pageTypePlural}/${parent_id}?revisions_count_error=1`,
9
+ };
10
+ }
11
+
4
12
  const { errors, pageErrors } = await ctx.client.pageRevisions.create(
5
13
  parent_id,
6
14
  ctx.locale